diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eac2c06..3a189790 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,18 +20,22 @@ jobs: contents: read steps: - name: Checkout code - uses: actions/checkout@v5 - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: ./go.mod - name: Run tests run: go test -v ./... + - name: Check for modernization opportunities + run: | + go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest ./... + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v21 + - name: Check Formatting run: nix flake check diff --git a/.github/workflows/patterns.yaml b/.github/workflows/patterns.yaml index 003b0d9f..103b593a 100644 --- a/.github/workflows/patterns.yaml +++ b/.github/workflows/patterns.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -32,7 +32,7 @@ jobs: - name: Upload Patterns Artifact if: steps.check-changes.outputs.changes == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: patterns path: patterns.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3bbc512c..faa3d1a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,12 +15,12 @@ jobs: contents: read steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: ./go.mod @@ -37,11 +37,11 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: ./go.mod - name: Run GoReleaser diff --git a/.github/workflows/update-version-and-create-tag.yml b/.github/workflows/update-version-and-create-tag.yml index 49cde729..83414571 100644 --- a/.github/workflows/update-version-and-create-tag.yml +++ b/.github/workflows/update-version-and-create-tag.yml @@ -24,17 +24,17 @@ concurrency: jobs: update-version: if: > - ${{ github.repository_owner == 'danielmiessler' }} && + github.repository_owner == 'danielmiessler' && github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main + uses: DeterminateSystems/nix-installer-action@v21 - name: Set up Git run: | diff --git a/cmd/generate_changelog/internal/changelog/generator.go b/cmd/generate_changelog/internal/changelog/generator.go index eee7c91b..939aa77d 100644 --- a/cmd/generate_changelog/internal/changelog/generator.go +++ b/cmd/generate_changelog/internal/changelog/generator.go @@ -574,8 +574,8 @@ func (g *Generator) extractChanges(pr *github.PR) []string { } if len(changes) == 0 && pr.Body != "" { - lines := strings.Split(pr.Body, "\n") - for _, line := range lines { + lines := strings.SplitSeq(pr.Body, "\n") + for line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ") { change := strings.TrimPrefix(strings.TrimPrefix(line, "- "), "* ") diff --git a/cmd/generate_changelog/internal/changelog/processing.go b/cmd/generate_changelog/internal/changelog/processing.go index c1988b8a..b8722e35 100644 --- a/cmd/generate_changelog/internal/changelog/processing.go +++ b/cmd/generate_changelog/internal/changelog/processing.go @@ -159,7 +159,7 @@ func (g *Generator) CreateNewChangelogEntry(version string) error { for _, file := range files { // Extract PR number from filename (e.g., "1640.txt" -> 1640) filename := filepath.Base(file) - if prNumStr := strings.TrimSuffix(filename, ".txt"); prNumStr != filename { + if prNumStr, ok := strings.CutSuffix(filename, ".txt"); ok { if prNum, err := strconv.Atoi(prNumStr); err == nil { processedPRs[prNum] = true prNumbers = append(prNumbers, prNum) diff --git a/internal/cli/flags.go b/internal/cli/flags.go index cf0adc04..1e564b9a 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strconv" "strings" @@ -230,8 +231,8 @@ func parseDebugLevel(args []string) int { if lvl, err := strconv.Atoi(args[i+1]); err == nil { return lvl } - } else if strings.HasPrefix(arg, "--debug=") { - if lvl, err := strconv.Atoi(strings.TrimPrefix(arg, "--debug=")); err == nil { + } else if after, ok := strings.CutPrefix(arg, "--debug="); ok { + if lvl, err := strconv.Atoi(after); err == nil { return lvl } } @@ -241,8 +242,8 @@ func parseDebugLevel(args []string) int { func extractFlag(arg string) string { var flag string - if strings.HasPrefix(arg, "--") { - flag = strings.TrimPrefix(arg, "--") + if after, ok := strings.CutPrefix(arg, "--"); ok { + flag = after if i := strings.Index(flag, "="); i > 0 { flag = flag[:i] } @@ -348,10 +349,8 @@ func validateImageFile(imagePath string) error { ext := strings.ToLower(filepath.Ext(imagePath)) validExtensions := []string{".png", ".jpeg", ".jpg", ".webp"} - for _, validExt := range validExtensions { - if ext == validExt { - return nil // Valid extension found - } + if slices.Contains(validExtensions, ext) { + return nil // Valid extension found } return fmt.Errorf("%s", fmt.Sprintf(i18n.T("invalid_image_file_extension"), ext)) @@ -370,13 +369,7 @@ func validateImageParameters(imagePath, size, quality, background string, compre // Validate size if size != "" { validSizes := []string{"1024x1024", "1536x1024", "1024x1536", "auto"} - valid := false - for _, validSize := range validSizes { - if size == validSize { - valid = true - break - } - } + valid := slices.Contains(validSizes, size) if !valid { return fmt.Errorf("%s", fmt.Sprintf(i18n.T("invalid_image_size"), size)) } @@ -385,13 +378,7 @@ func validateImageParameters(imagePath, size, quality, background string, compre // Validate quality if quality != "" { validQualities := []string{"low", "medium", "high", "auto"} - valid := false - for _, validQuality := range validQualities { - if quality == validQuality { - valid = true - break - } - } + valid := slices.Contains(validQualities, quality) if !valid { return fmt.Errorf("%s", fmt.Sprintf(i18n.T("invalid_image_quality"), quality)) } @@ -400,13 +387,7 @@ func validateImageParameters(imagePath, size, quality, background string, compre // Validate background if background != "" { validBackgrounds := []string{"opaque", "transparent"} - valid := false - for _, validBackground := range validBackgrounds { - if background == validBackground { - valid = true - break - } - } + valid := slices.Contains(validBackgrounds, background) if !valid { return fmt.Errorf("%s", fmt.Sprintf(i18n.T("invalid_image_background"), background)) } diff --git a/internal/cli/help.go b/internal/cli/help.go index 2b8e5c02..26549da6 100644 --- a/internal/cli/help.go +++ b/internal/cli/help.go @@ -183,10 +183,10 @@ func detectLanguageFromArgs() string { if i+1 < len(args) { return args[i+1] } - } else if strings.HasPrefix(arg, "--language=") { - return strings.TrimPrefix(arg, "--language=") - } else if strings.HasPrefix(arg, "-g=") { - return strings.TrimPrefix(arg, "-g=") + } else if after, ok := strings.CutPrefix(arg, "--language="); ok { + return after + } else if after, ok := strings.CutPrefix(arg, "-g="); ok { + return after } else if runtime.GOOS == "windows" && strings.HasPrefix(arg, "/g:") { return strings.TrimPrefix(arg, "/g:") } else if runtime.GOOS == "windows" && strings.HasPrefix(arg, "/g=") { @@ -272,10 +272,7 @@ func (h *TranslatedHelpWriter) writeAllFlags() { // Pad to align descriptions flagStr := flagLine.String() - padding := 34 - len(flagStr) - if padding < 2 { - padding = 2 - } + padding := max(34-len(flagStr), 2) fmt.Fprintf(h.writer, "%s%s%s", flagStr, strings.Repeat(" ", padding), description) diff --git a/internal/cli/output.go b/internal/cli/output.go index ea64f016..da13e1a3 100644 --- a/internal/cli/output.go +++ b/internal/cli/output.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "github.com/atotto/clipboard" @@ -66,10 +67,5 @@ func CreateAudioOutputFile(audioData []byte, fileName string) (err error) { func IsAudioFormat(fileName string) bool { ext := strings.ToLower(filepath.Ext(fileName)) audioExts := []string{".wav", ".mp3", ".m4a", ".aac", ".ogg", ".flac"} - for _, audioExt := range audioExts { - if ext == audioExt { - return true - } - } - return false + return slices.Contains(audioExts, ext) } diff --git a/internal/domain/file_manager.go b/internal/domain/file_manager.go index 1d7a27fa..64753e4b 100644 --- a/internal/domain/file_manager.go +++ b/internal/domain/file_manager.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" ) @@ -146,14 +147,7 @@ func fixInvalidEscapes(jsonStr string) string { // Check for escape sequences only inside strings if inQuotes && ch == '\\' && i+1 < len(jsonStr) { nextChar := jsonStr[i+1] - isValid := false - - for _, validEscape := range validEscapes { - if nextChar == validEscape { - isValid = true - break - } - } + isValid := slices.Contains(validEscapes, nextChar) if !isValid { // Invalid escape sequence - add an extra backslash diff --git a/internal/plugins/ai/gemini/voices.go b/internal/plugins/ai/gemini/voices.go index 66f8b7ba..0f3e9839 100644 --- a/internal/plugins/ai/gemini/voices.go +++ b/internal/plugins/ai/gemini/voices.go @@ -3,6 +3,7 @@ package gemini import ( "fmt" "sort" + "strings" ) // GeminiVoice represents a Gemini TTS voice with its characteristics @@ -126,16 +127,17 @@ func ListGeminiVoices(shellCompleteMode bool) string { if shellCompleteMode { // For shell completion, just return voice names names := GetGeminiVoiceNames() - result := "" + var result strings.Builder for _, name := range names { - result += name + "\n" + result.WriteString(name + "\n") } - return result + return result.String() } // For human-readable output voices := GetGeminiVoices() - result := "Available Gemini Text-to-Speech voices:\n\n" + var result strings.Builder + result.WriteString("Available Gemini Text-to-Speech voices:\n\n") // Group by characteristics for better readability groups := map[string][]GeminiVoice{ @@ -186,22 +188,22 @@ func ListGeminiVoices(shellCompleteMode bool) string { // Output grouped voices for groupName, groupVoices := range groups { if len(groupVoices) > 0 { - result += fmt.Sprintf("%s:\n", groupName) + result.WriteString(fmt.Sprintf("%s:\n", groupName)) for _, voice := range groupVoices { defaultStr := "" if voice.Name == "Kore" { defaultStr = " (default)" } - result += fmt.Sprintf(" %-15s - %s%s\n", voice.Name, voice.Description, defaultStr) + result.WriteString(fmt.Sprintf(" %-15s - %s%s\n", voice.Name, voice.Description, defaultStr)) } - result += "\n" + result.WriteString("\n") } } - result += "Use --voice to select a specific voice.\n" - result += "Example: fabric --voice Charon -m gemini-2.5-flash-preview-tts -o output.wav \"Hello world\"\n" + result.WriteString("Use --voice to select a specific voice.\n") + result.WriteString("Example: fabric --voice Charon -m gemini-2.5-flash-preview-tts -o output.wav \"Hello world\"\n") - return result + return result.String() } // NOTE: This implementation maintains a curated list based on official Google documentation. diff --git a/internal/plugins/ai/lmstudio/lmstudio.go b/internal/plugins/ai/lmstudio/lmstudio.go index 804a4b21..8e64b88f 100644 --- a/internal/plugins/ai/lmstudio/lmstudio.go +++ b/internal/plugins/ai/lmstudio/lmstudio.go @@ -140,8 +140,8 @@ func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.Cha continue } - if bytes.HasPrefix(line, []byte("data: ")) { - line = bytes.TrimPrefix(line, []byte("data: ")) + if after, ok := bytes.CutPrefix(line, []byte("data: ")); ok { + line = after } if string(line) == "[DONE]" { diff --git a/internal/plugins/ai/openai/openai_image.go b/internal/plugins/ai/openai/openai_image.go index 4c04c26a..872cea4b 100644 --- a/internal/plugins/ai/openai/openai_image.go +++ b/internal/plugins/ai/openai/openai_image.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "github.com/danielmiessler/fabric/internal/domain" @@ -31,12 +32,7 @@ var ImageGenerationSupportedModels = []string{ // supportsImageGeneration checks if the given model supports the image_generation tool func supportsImageGeneration(model string) bool { - for _, supportedModel := range ImageGenerationSupportedModels { - if model == supportedModel { - return true - } - } - return false + return slices.Contains(ImageGenerationSupportedModels, model) } // getOutputFormatFromExtension determines the API output format based on file extension diff --git a/internal/plugins/ai/perplexity/perplexity.go b/internal/plugins/ai/perplexity/perplexity.go index 2e3d88b7..3f0a6198 100644 --- a/internal/plugins/ai/perplexity/perplexity.go +++ b/internal/plugins/ai/perplexity/perplexity.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "sync" "github.com/danielmiessler/fabric/internal/domain" @@ -107,18 +108,19 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o return "", fmt.Errorf("perplexity API request failed: %w", err) // Corrected capitalization } - content := resp.GetLastContent() + var content strings.Builder + content.WriteString(resp.GetLastContent()) // Append citations if available citations := resp.GetCitations() if len(citations) > 0 { - content += "\n\n# CITATIONS\n\n" + content.WriteString("\n\n# CITATIONS\n\n") for i, citation := range citations { - content += fmt.Sprintf("- [%d] %s\n", i+1, citation) + content.WriteString(fmt.Sprintf("- [%d] %s\n", i+1, citation)) } } - return content, nil + return content.String(), nil } func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan string) error { diff --git a/internal/plugins/template/extension_executor_test.go b/internal/plugins/template/extension_executor_test.go index 4f9c2dfe..44940828 100644 --- a/internal/plugins/template/extension_executor_test.go +++ b/internal/plugins/template/extension_executor_test.go @@ -189,7 +189,8 @@ esac` // Helper function to create and register extension createExtension := func(name, opName, cmdTemplate string, config map[string]any) error { configPath := filepath.Join(tmpDir, name+".yaml") - configContent := `name: ` + name + ` + var configContent strings.Builder + configContent.WriteString(`name: ` + name + ` executable: ` + testScript + ` type: executable timeout: 30s @@ -199,14 +200,14 @@ operations: config: output: method: file - file_config:` + file_config:`) // Add config options for k, v := range config { - configContent += "\n " + k + ": " + strings.TrimSpace(v.(string)) + configContent.WriteString("\n " + k + ": " + strings.TrimSpace(v.(string))) } - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(configContent.String()), 0644); err != nil { return err } diff --git a/internal/server/ollama.go b/internal/server/ollama.go index 430aba5e..763d5aa6 100644 --- a/internal/server/ollama.go +++ b/internal/server/ollama.go @@ -102,7 +102,7 @@ func ServeOllama(registry *core.PluginRegistry, address string, version string) // Ollama Endpoints r.GET("/api/tags", typeConversion.ollamaTags) r.GET("/api/version", func(c *gin.Context) { - c.Data(200, "application/json", []byte(fmt.Sprintf("{\"%s\"}", version))) + c.Data(200, "application/json", fmt.Appendf(nil, "{\"%s\"}", version)) }) r.POST("/api/chat", typeConversion.ollamaChat) @@ -224,7 +224,7 @@ func (f APIConvert) ollamaChat(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "testing endpoint"}) return } - for _, word := range strings.Split(fabricResponse.Content, " ") { + for word := range strings.SplitSeq(fabricResponse.Content, " ") { forwardedResponse = OllamaResponse{ Model: "", CreatedAt: "",