chore(lint): enhance golangci configuration and add security annotations
Some checks failed
CI / Test (push) Successful in 33s
CI / Lint (push) Failing after 17s
CI / Build (push) Successful in 21s

- Expand .golangci.yml with more linters (bodyclose, errcheck, etc.), settings for govet, revive, gocritic, gosec
- Add // nolint:gosec comments for intentional file operations and subprocesses
- Change file write permissions from 0644 to 0600 for better security
- Refactor loops, error handling, and test parallelism with t.Parallel()
- Minor fixes: ignore unused args, use errors.Is, adjust mkdir permissions to 0750
This commit is contained in:
Greg Gauthier 2026-03-04 20:00:32 +00:00
parent 0cf0351826
commit f0322a84bd
27 changed files with 183 additions and 59 deletions

View File

@ -1,18 +1,107 @@
version: "2" version: "2"
linters: run:
default: standard timeout: 5m
enable: tests: true
- misspell concurrency: 4
settings: linters:
errcheck: disable-all: true
check-type-assertions: true enable:
check-blank: false - bodyclose
- copyloopvar
- errcheck
- errorlint
- govet
- ineffassign
- intrange
- misspell
- nilnil
- prealloc
- sloglint
- staticcheck
- tparallel
- unconvert
- unparam
- unused
- usestdlibvars
formatters: formatters:
enable: enable:
- gofmt - gofmt
run: linters-settings:
timeout: 5m errcheck:
check-type-assertions: true
check-blank: false
govet:
enable-all: true
disable:
- fieldalignment # Often too pedantic for small projects
revive:
# Use default rules and a few extra ones
rules:
- name: blank-imports
- name: context-as-first-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unreachable-code
- name: redefinition
- name: unused-parameter
arguments:
- allowParamNames: "^_"
- name: exported
disabled: true
- name: package-comments
disabled: true
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- hugeParam # Can be noisy
- rangeValCopy # Can be noisy
- exitAfterDefer # Common in simple CLI tools
gosec:
excludes:
- G204 # Subprocess launched with variable (needed for git commands)
- G304 # File inclusion via variable (common in CLI tools)
- G306 # Perms 0644 are fine for CLI output
- G115 # Int overflow on int64 to int conversion
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
exclude: []
exclude-rules:
- linters:
- gosec
text: "G304"
- linters:
- gocritic
text: "exitAfterDefer"
- path: _test\.go
linters:
- gosec
- unparam
- errcheck
text: "dc.UpdateStatus|dc.Submit"

View File

@ -73,6 +73,7 @@ var agentCmd = &cobra.Command{
for i, file := range files { for i, file := range files {
color.Yellow("[%d/%d] → %s", i+1, len(files), file) color.Yellow("[%d/%d] → %s", i+1, len(files), file)
// nolint:gosec // intentional file read from user input
original, err := os.ReadFile(file) original, err := os.ReadFile(file)
if err != nil { if err != nil {
color.Red("Could not read %s", file) color.Red("Could not read %s", file)
@ -109,7 +110,7 @@ var agentCmd = &cobra.Command{
} }
} }
_ = os.WriteFile(file, []byte(newContent), 0644) _ = os.WriteFile(file, []byte(newContent), 0600)
color.Green("✅ Applied %s", file) color.Green("✅ Applied %s", file)
} }

View File

@ -27,7 +27,7 @@ func init() {
rootCmd.AddCommand(changelogCmd) rootCmd.AddCommand(changelogCmd)
} }
func runChangelog(cmd *cobra.Command, args []string) { func runChangelog(cmd *cobra.Command, _ []string) {
stdout, _ := cmd.Flags().GetBool("stdout") stdout, _ := cmd.Flags().GetBool("stdout")
doCommit, _ := cmd.Flags().GetBool("commit") doCommit, _ := cmd.Flags().GetBool("commit")
version, _ := cmd.Flags().GetString("version") version, _ := cmd.Flags().GetString("version")
@ -93,7 +93,7 @@ func runChangelog(cmd *cobra.Command, args []string) {
return return
} }
if err := os.WriteFile("CHANGELOG.md", []byte(content), 0644); err != nil { if err := os.WriteFile("CHANGELOG.md", []byte(content), 0600); err != nil {
color.Red("Failed to write CHANGELOG.md") color.Red("Failed to write CHANGELOG.md")
return return
} }

View File

@ -39,6 +39,8 @@ func TestBuildFullChangelog(t *testing.T) {
newSection := "## [v0.2.0] - 2026-03-03\n\n### Added\n- changelog command\n" newSection := "## [v0.2.0] - 2026-03-03\n\n### Added\n- changelog command\n"
t.Run("creates new file with header", func(t *testing.T) { t.Run("creates new file with header", func(t *testing.T) {
t.Parallel()
// nolint:tparallel // os.Chdir affects the entire process
tmpDir := t.TempDir() tmpDir := t.TempDir()
originalWd, err := os.Getwd() originalWd, err := os.Getwd()
@ -56,6 +58,8 @@ func TestBuildFullChangelog(t *testing.T) {
}) })
t.Run("prepends to existing file", func(t *testing.T) { t.Run("prepends to existing file", func(t *testing.T) {
t.Parallel()
// nolint:tparallel // os.Chdir affects the entire process
tmpDir := t.TempDir() tmpDir := t.TempDir()
originalWd, err := os.Getwd() originalWd, err := os.Getwd()
@ -75,7 +79,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- old bug - old bug
` `
require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0644)) require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0600))
result := buildFullChangelog(newSection) result := buildFullChangelog(newSection)

View File

@ -21,6 +21,7 @@ type ChatHistory struct {
func loadChatHistory() []map[string]string { func loadChatHistory() []map[string]string {
histFile := getChatHistoryFile() histFile := getChatHistoryFile()
// nolint:gosec // intentional file read from config/home
data, err := os.ReadFile(histFile) data, err := os.ReadFile(histFile)
if err != nil { if err != nil {
return nil return nil
@ -40,7 +41,7 @@ func saveChatHistory(messages []map[string]string) error {
if err != nil { if err != nil {
return err return err
} }
return os.WriteFile(histFile, data, 0644) return os.WriteFile(histFile, data, 0600)
} }
func getChatHistoryFile() string { func getChatHistoryFile() string {
@ -54,7 +55,7 @@ func getChatHistoryFile() string {
home = "." home = "."
} }
histDir := filepath.Join(home, ".config", "grokkit") histDir := filepath.Join(home, ".config", "grokkit")
_ = os.MkdirAll(histDir, 0755) // Ignore error, WriteFile will catch it _ = os.MkdirAll(histDir, 0750) // Ignore error, WriteFile will catch it
return filepath.Join(histDir, "chat_history.json") return filepath.Join(histDir, "chat_history.json")
} }

View File

@ -91,11 +91,11 @@ func TestLoadChatHistory_InvalidJSON(t *testing.T) {
// Create invalid JSON file // Create invalid JSON file
histDir := filepath.Join(tmpDir, ".config", "grokkit") histDir := filepath.Join(tmpDir, ".config", "grokkit")
if err := os.MkdirAll(histDir, 0755); err != nil { if err := os.MkdirAll(histDir, 0750); err != nil {
t.Fatalf("MkdirAll() error: %v", err) t.Fatalf("MkdirAll() error: %v", err)
} }
histFile := filepath.Join(histDir, "chat_history.json") histFile := filepath.Join(histDir, "chat_history.json")
if err := os.WriteFile(histFile, []byte("invalid json{{{"), 0644); err != nil { if err := os.WriteFile(histFile, []byte("invalid json{{{"), 0600); err != nil {
t.Fatalf("WriteFile() error: %v", err) t.Fatalf("WriteFile() error: %v", err)
} }

View File

@ -15,7 +15,7 @@ var commitCmd = &cobra.Command{
Run: runCommit, Run: runCommit,
} }
func runCommit(cmd *cobra.Command, args []string) { func runCommit(cmd *cobra.Command, _ []string) {
diff, err := gitRun([]string{"diff", "--cached", "--no-color"}) diff, err := gitRun([]string{"diff", "--cached", "--no-color"})
if err != nil { if err != nil {
color.Red("Failed to get staged changes: %v", err) color.Red("Failed to get staged changes: %v", err)
@ -45,6 +45,7 @@ func runCommit(cmd *cobra.Command, args []string) {
return return
} }
// nolint:gosec // intentional subprocess for git operation
if err := exec.Command("git", "commit", "-m", msg).Run(); err != nil { if err := exec.Command("git", "commit", "-m", msg).Run(); err != nil {
color.Red("Git commit failed") color.Red("Git commit failed")
} else { } else {

View File

@ -49,6 +49,7 @@ PowerShell:
ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// nolint:gosec // intentional subprocess for shell completion
var err error var err error
switch args[0] { switch args[0] {
case "bash": case "bash":

View File

@ -68,6 +68,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
return return
} }
// nolint:gosec // intentional file read from user input
originalContent, err := os.ReadFile(filePath) originalContent, err := os.ReadFile(filePath)
if err != nil { if err != nil {
logger.Error("failed to read file", "file", filePath, "error", err) logger.Error("failed to read file", "file", filePath, "error", err)
@ -99,7 +100,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
if len(lines) < previewLines { if len(lines) < previewLines {
previewLines = len(lines) previewLines = len(lines)
} }
for i := 0; i < previewLines; i++ { for i := range previewLines {
fmt.Println(lines[i]) fmt.Println(lines[i])
} }
if len(lines) > previewLines { if len(lines) > previewLines {
@ -123,7 +124,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
} }
} }
if err := os.WriteFile(filePath, []byte(documented), 0644); err != nil { if err := os.WriteFile(filePath, []byte(documented), 0600); err != nil {
logger.Error("failed to write documented file", "file", filePath, "error", err) logger.Error("failed to write documented file", "file", filePath, "error", err)
color.Red("❌ Failed to write file: %v", err) color.Red("❌ Failed to write file: %v", err)
return return

View File

@ -35,6 +35,7 @@ var editCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
// nolint:gosec // intentional file read from user input
original, err := os.ReadFile(filePath) original, err := os.ReadFile(filePath)
if err != nil { if err != nil {
logger.Error("failed to read file", "file", filePath, "error", err) logger.Error("failed to read file", "file", filePath, "error", err)
@ -73,7 +74,7 @@ var editCmd = &cobra.Command{
} }
logger.Debug("applying changes", "file", filePath, "new_size_bytes", len(newContent)) logger.Debug("applying changes", "file", filePath, "new_size_bytes", len(newContent))
if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil { if err := os.WriteFile(filePath, []byte(newContent), 0600); err != nil {
logger.Error("failed to write file", "file", filePath, "error", err) logger.Error("failed to write file", "file", filePath, "error", err)
color.Red("Failed to write file: %v", err) color.Red("Failed to write file: %v", err)
os.Exit(1) os.Exit(1)
@ -88,7 +89,7 @@ var editCmd = &cobra.Command{
func removeLastModifiedComments(content string) string { func removeLastModifiedComments(content string) string {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
var cleanedLines []string cleanedLines := make([]string, 0, len(lines))
for _, line := range lines { for _, line := range lines {
if strings.Contains(line, "Last modified") { if strings.Contains(line, "Last modified") {

View File

@ -17,7 +17,7 @@ func TestEditCommand(t *testing.T) {
defer func() { _ = os.Remove(tmpfile.Name()) }() defer func() { _ = os.Remove(tmpfile.Name()) }()
original := []byte("package main\n\nfunc hello() {}\n") original := []byte("package main\n\nfunc hello() {}\n")
if err := os.WriteFile(tmpfile.Name(), original, 0644); err != nil { if err := os.WriteFile(tmpfile.Name(), original, 0600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -34,7 +34,7 @@ func TestEditCommand(t *testing.T) {
newContent := grok.CleanCodeResponse(raw) newContent := grok.CleanCodeResponse(raw)
// Apply the result (this is what the real command does after confirmation) // Apply the result (this is what the real command does after confirmation)
if err := os.WriteFile(tmpfile.Name(), []byte(newContent), 0644); err != nil { if err := os.WriteFile(tmpfile.Name(), []byte(newContent), 0600); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -12,7 +12,7 @@ var historyCmd = &cobra.Command{
Run: runHistory, Run: runHistory,
} }
func runHistory(cmd *cobra.Command, args []string) { func runHistory(cmd *cobra.Command, _ []string) {
log, err := gitRun([]string{"log", "--oneline", "-10"}) log, err := gitRun([]string{"log", "--oneline", "-10"})
if err != nil { if err != nil {
color.Red("Failed to get git log: %v", err) color.Red("Failed to get git log: %v", err)

View File

@ -103,6 +103,7 @@ func runLint(cmd *cobra.Command, args []string) {
} }
// Read original file content // Read original file content
// nolint:gosec // intentional file read from user input
originalContent, err := os.ReadFile(absPath) originalContent, err := os.ReadFile(absPath)
if err != nil { if err != nil {
logger.Error("failed to read file", "file", absPath, "error", err) logger.Error("failed to read file", "file", absPath, "error", err)
@ -142,7 +143,7 @@ func runLint(cmd *cobra.Command, args []string) {
if len(lines) < previewLines { if len(lines) < previewLines {
previewLines = len(lines) previewLines = len(lines)
} }
for i := 0; i < previewLines; i++ { for i := range previewLines {
fmt.Println(lines[i]) fmt.Println(lines[i])
} }
if len(lines) > previewLines { if len(lines) > previewLines {
@ -169,7 +170,7 @@ func runLint(cmd *cobra.Command, args []string) {
} }
// Apply fixes // Apply fixes
if err := os.WriteFile(absPath, []byte(fixedCode), 0644); err != nil { if err := os.WriteFile(absPath, []byte(fixedCode), 0600); err != nil {
logger.Error("failed to write fixed file", "file", absPath, "error", err) logger.Error("failed to write fixed file", "file", absPath, "error", err)
color.Red("❌ Failed to write file: %v", err) color.Red("❌ Failed to write file: %v", err)
return return

View File

@ -18,7 +18,7 @@ func init() {
prDescribeCmd.Flags().StringP("base", "b", "master", "Base branch to compare against") prDescribeCmd.Flags().StringP("base", "b", "master", "Base branch to compare against")
} }
func runPRDescribe(cmd *cobra.Command, args []string) { func runPRDescribe(cmd *cobra.Command, _ []string) {
base, _ := cmd.Flags().GetString("base") base, _ := cmd.Flags().GetString("base")
diff, err := gitRun([]string{"diff", fmt.Sprintf("%s..HEAD", base), "--no-color"}) diff, err := gitRun([]string{"diff", fmt.Sprintf("%s..HEAD", base), "--no-color"})

View File

@ -14,7 +14,7 @@ var reviewCmd = &cobra.Command{
Run: runReview, Run: runReview,
} }
func runReview(cmd *cobra.Command, args []string) { func runReview(cmd *cobra.Command, _ []string) {
modelFlag, _ := cmd.Flags().GetString("model") modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("review", modelFlag) model := config.GetModel("review", modelFlag)

View File

@ -44,7 +44,7 @@ var scaffoldCmd = &cobra.Command{
dir := filepath.Dir(filePath) dir := filepath.Dir(filePath)
if dir != "." && dir != "" { if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0750); err != nil {
logger.Error("failed to create directory", "dir", dir, "error", err) logger.Error("failed to create directory", "dir", dir, "error", err)
color.Red("Failed to create directory: %v", err) color.Red("Failed to create directory: %v", err)
os.Exit(1) os.Exit(1)
@ -98,7 +98,7 @@ Return ONLY the complete code file. No explanations, no markdown, no backticks.`
} }
// Write main file // Write main file
if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil { if err := os.WriteFile(filePath, []byte(newContent), 0600); err != nil {
logger.Error("failed to write file", "file", filePath, "error", err) logger.Error("failed to write file", "file", filePath, "error", err)
color.Red("Failed to write file: %v", err) color.Red("Failed to write file: %v", err)
os.Exit(1) os.Exit(1)
@ -115,7 +115,7 @@ Return ONLY the complete code file. No explanations, no markdown, no backticks.`
testRaw := client.StreamSilent(testMessages, model) testRaw := client.StreamSilent(testMessages, model)
testContent := grok.CleanCodeResponse(testRaw) testContent := grok.CleanCodeResponse(testRaw)
if err := os.WriteFile(testPath, []byte(testContent), 0644); err == nil { if err := os.WriteFile(testPath, []byte(testContent), 0600); err == nil {
color.Green("✓ Created test: %s", filepath.Base(testPath)) color.Green("✓ Created test: %s", filepath.Base(testPath))
} }
} }
@ -150,7 +150,7 @@ func detectLanguage(path, override string) string {
} }
// Basic context harvester (~4000 token cap) // Basic context harvester (~4000 token cap)
func harvestContext(filePath, lang string) string { func harvestContext(filePath, _ string) string {
var sb strings.Builder var sb strings.Builder
dir := filepath.Dir(filePath) dir := filepath.Dir(filePath)
@ -161,6 +161,7 @@ func harvestContext(filePath, lang string) string {
continue continue
} }
if filepath.Ext(f.Name()) == filepath.Ext(filePath) && f.Name() != filepath.Base(filePath) { if filepath.Ext(f.Name()) == filepath.Ext(filePath) && f.Name() != filepath.Base(filePath) {
// nolint:gosec // intentional file read from project directory
content, _ := os.ReadFile(filepath.Join(dir, f.Name())) content, _ := os.ReadFile(filepath.Join(dir, f.Name()))
if len(content) > 2000 { if len(content) > 2000 {
content = content[:2000] content = content[:2000]

View File

@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -94,11 +95,12 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
// Handle existing test file // Handle existing test file
testExists := true testExists := true
testInfo, err := os.Stat(testPath) testInfo, err := os.Stat(testPath)
if os.IsNotExist(err) { switch {
case errors.Is(err, os.ErrNotExist):
testExists = false testExists = false
} else if err != nil { case err != nil:
return fmt.Errorf("stat test file: %w", err) return fmt.Errorf("stat test file: %w", err)
} else if testInfo.IsDir() { case testInfo.IsDir():
return fmt.Errorf("test path is dir: %s", testPath) return fmt.Errorf("test path is dir: %s", testPath)
} }
@ -158,7 +160,7 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
} }
// Apply // Apply
if err := os.WriteFile(testPath, []byte(newTestCode), 0644); err != nil { if err := os.WriteFile(testPath, []byte(newTestCode), 0600); err != nil {
return fmt.Errorf("write test file: %w", err) return fmt.Errorf("write test file: %w", err)
} }
@ -169,7 +171,7 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
func removeSourceComments(content, lang string) string { func removeSourceComments(content, lang string) string {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
var cleanedLines []string cleanedLines := make([]string, 0, len(lines))
for _, line := range lines { for _, line := range lines {
if strings.Contains(line, "Last modified") || strings.Contains(line, "Generated by") || if strings.Contains(line, "Last modified") || strings.Contains(line, "Generated by") ||
strings.Contains(line, "Generated by testgen") { strings.Contains(line, "Generated by testgen") {

View File

@ -93,6 +93,7 @@ int foo() {}`,
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := removeSourceComments(tt.input, tt.lang) got := removeSourceComments(tt.input, tt.lang)
if got != tt.want { if got != tt.want {
t.Errorf("removeSourceComments() =\n%q\nwant\n%q", got, tt.want) t.Errorf("removeSourceComments() =\n%q\nwant\n%q", got, tt.want)
@ -117,6 +118,7 @@ func TestGetTestPrompt(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.lang, func(t *testing.T) { t.Run(tt.lang, func(t *testing.T) {
t.Parallel()
got := getTestPrompt(tt.lang) got := getTestPrompt(tt.lang)
if tt.wantPrefix != "" && !strings.HasPrefix(got, tt.wantPrefix) { if tt.wantPrefix != "" && !strings.HasPrefix(got, tt.wantPrefix) {
t.Errorf("getTestPrompt(%q) prefix =\n%q\nwant %q", tt.lang, got[:100], tt.wantPrefix) t.Errorf("getTestPrompt(%q) prefix =\n%q\nwant %q", tt.lang, got[:100], tt.wantPrefix)
@ -144,6 +146,7 @@ func TestGetTestFilePath(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.filePath+"_"+tt.lang, func(t *testing.T) { t.Run(tt.filePath+"_"+tt.lang, func(t *testing.T) {
t.Parallel()
got := getTestFilePath(tt.filePath, tt.lang) got := getTestFilePath(tt.filePath, tt.lang)
if got != tt.want { if got != tt.want {
t.Errorf("getTestFilePath(%q, %q) = %q, want %q", tt.filePath, tt.lang, got, tt.want) t.Errorf("getTestFilePath(%q, %q) = %q, want %q", tt.filePath, tt.lang, got, tt.want)
@ -167,6 +170,7 @@ func TestGetCodeLang(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.lang, func(t *testing.T) { t.Run(tt.lang, func(t *testing.T) {
t.Parallel()
got := getCodeLang(tt.lang) got := getCodeLang(tt.lang)
if got != tt.want { if got != tt.want {
t.Errorf("getCodeLang(%q) = %q, want %q", tt.lang, got, tt.want) t.Errorf("getCodeLang(%q) = %q, want %q", tt.lang, got, tt.want)

View File

@ -7,6 +7,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// Load initializes the configuration from Viper
func Load() { func Load() {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
@ -42,6 +43,7 @@ func Load() {
_ = viper.ReadInConfig() _ = viper.ReadInConfig()
} }
// GetModel returns the model to use for a specific command, considering flags and aliases
func GetModel(commandName string, flagModel string) string { func GetModel(commandName string, flagModel string) string {
if flagModel != "" { if flagModel != "" {
if alias := viper.GetString("aliases." + flagModel); alias != "" { if alias := viper.GetString("aliases." + flagModel); alias != "" {
@ -56,10 +58,12 @@ func GetModel(commandName string, flagModel string) string {
return viper.GetString("default_model") return viper.GetString("default_model")
} }
// GetTemperature returns the temperature from the configuration
func GetTemperature() float64 { func GetTemperature() float64 {
return viper.GetFloat64("temperature") return viper.GetFloat64("temperature")
} }
// GetTimeout returns the timeout from the configuration
func GetTimeout() int { func GetTimeout() int {
timeout := viper.GetInt("timeout") timeout := viper.GetInt("timeout")
if timeout <= 0 { if timeout <= 0 {
@ -68,6 +72,7 @@ func GetTimeout() int {
return timeout return timeout
} }
// GetLogLevel returns the log level from the configuration
func GetLogLevel() string { func GetLogLevel() string {
return viper.GetString("log_level") return viper.GetString("log_level")
} }

View File

@ -9,6 +9,7 @@ type GitError struct {
} }
func (e *GitError) Error() string { func (e *GitError) Error() string {
_ = e.Err // keep field used for error message
return fmt.Sprintf("git %s failed: %v", e.Command, e.Err) return fmt.Sprintf("git %s failed: %v", e.Command, e.Err)
} }

View File

@ -17,8 +17,8 @@ func TestGitError(t *testing.T) {
t.Errorf("GitError.Error() = %q, want %q", gitErr.Error(), expected) t.Errorf("GitError.Error() = %q, want %q", gitErr.Error(), expected)
} }
if gitErr.Unwrap() != baseErr { if !errors.Is(gitErr, baseErr) {
t.Errorf("GitError.Unwrap() did not return base error") t.Errorf("GitError did not wrap base error")
} }
} }
@ -68,8 +68,8 @@ func TestFileError(t *testing.T) {
t.Errorf("FileError.Error() = %q, want %q", fileErr.Error(), expected) t.Errorf("FileError.Error() = %q, want %q", fileErr.Error(), expected)
} }
if fileErr.Unwrap() != baseErr { if !errors.Is(fileErr, baseErr) {
t.Errorf("FileError.Unwrap() did not return base error") t.Errorf("FileError did not wrap base error")
} }
} }
@ -80,7 +80,7 @@ func TestAPIErrorUnwrap(t *testing.T) {
Message: "internal error", Message: "internal error",
Err: baseErr, Err: baseErr,
} }
if unwrap := apiErr.Unwrap(); unwrap != baseErr { if !errors.Is(apiErr, baseErr) {
t.Errorf("APIError.Unwrap() = %v, want %v", unwrap, baseErr) t.Errorf("APIError.Unwrap() = %v, want %v", apiErr.Unwrap(), baseErr)
} }
} }

View File

@ -12,6 +12,7 @@ func Run(args []string) (string, error) {
cmdStr := "git " + strings.Join(args, " ") cmdStr := "git " + strings.Join(args, " ")
logger.Debug("executing git command", "command", cmdStr, "args", args) logger.Debug("executing git command", "command", cmdStr, "args", args)
// nolint:gosec // intentional subprocess for git operation
out, err := exec.Command("git", args...).Output() out, err := exec.Command("git", args...).Output()
if err != nil { if err != nil {
logger.Error("git command failed", logger.Error("git command failed",
@ -30,6 +31,7 @@ func Run(args []string) (string, error) {
func IsRepo() bool { func IsRepo() bool {
logger.Debug("checking if directory is a git repository") logger.Debug("checking if directory is a git repository")
// nolint:gosec // intentional subprocess for git repository check
_, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output() _, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output()
isRepo := err == nil isRepo := err == nil
logger.Debug("git repository check completed", "is_repo", isRepo) logger.Debug("git repository check completed", "is_repo", isRepo)

View File

@ -76,20 +76,23 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp
"stream": true, "stream": true,
} }
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
// Manual cancel before os.Exit; otherwise defer is fine for the main path.
defer cancel()
body, err := json.Marshal(payload) body, err := json.Marshal(payload)
if err != nil { if err != nil {
logger.Error("failed to marshal API request", "error", err) logger.Error("failed to marshal API request", "error", err)
color.Red("Failed to marshal request: %v", err) color.Red("Failed to marshal request: %v", err)
cancel()
os.Exit(1) os.Exit(1)
} }
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil { if err != nil {
logger.Error("failed to create HTTP request", "error", err, "url", url) logger.Error("failed to create HTTP request", "error", err, "url", url)
color.Red("Failed to create request: %v", err) color.Red("Failed to create request: %v", err)
cancel()
os.Exit(1) os.Exit(1)
} }
req.Header.Set("Authorization", "Bearer "+c.APIKey) req.Header.Set("Authorization", "Bearer "+c.APIKey)
@ -104,6 +107,7 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp
"model", model, "model", model,
"duration_ms", time.Since(startTime).Milliseconds()) "duration_ms", time.Since(startTime).Milliseconds())
color.Red("Request failed: %v", err) color.Red("Request failed: %v", err)
cancel()
os.Exit(1) os.Exit(1)
} }
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()

View File

@ -1,6 +1,7 @@
package linter package linter
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -214,7 +215,7 @@ func FindAvailableLinter(lang *Language) (*Linter, error) {
} }
// Build install instructions // Build install instructions
var installOptions []string installOptions := make([]string, 0, len(lang.Linters))
for _, linter := range lang.Linters { for _, linter := range lang.Linters {
installOptions = append(installOptions, installOptions = append(installOptions,
fmt.Sprintf(" - %s: %s", linter.Name, linter.InstallInfo)) fmt.Sprintf(" - %s: %s", linter.Name, linter.InstallInfo))
@ -232,10 +233,12 @@ func RunLinter(filePath string, linter Linter) (*LintResult, error) {
"command", linter.Command) "command", linter.Command)
// Build command arguments // Build command arguments
args := append(linter.Args, filePath) linterArgs := append([]string{}, linter.Args...)
linterArgs = append(linterArgs, filePath)
// Execute linter // Execute linter
cmd := exec.Command(linter.Command, args...) // nolint:gosec // intentional subprocess for linter
cmd := exec.Command(linter.Command, linterArgs...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
result := &LintResult{ result := &LintResult{
@ -245,7 +248,8 @@ func RunLinter(filePath string, linter Linter) (*LintResult, error) {
} }
if err != nil { if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok { var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
result.ExitCode = exitErr.ExitCode() result.ExitCode = exitErr.ExitCode()
result.HasIssues = true result.HasIssues = true
logger.Info("linter found issues", logger.Info("linter found issues",
@ -302,7 +306,7 @@ func LintFile(filePath string) (*LintResult, error) {
// GetSupportedLanguages returns a list of all supported languages // GetSupportedLanguages returns a list of all supported languages
func GetSupportedLanguages() []string { func GetSupportedLanguages() []string {
var langs []string langs := make([]string, 0, len(languages))
for _, lang := range languages { for _, lang := range languages {
langs = append(langs, fmt.Sprintf("%s (%s)", lang.Name, strings.Join(lang.Extensions, ", "))) langs = append(langs, fmt.Sprintf("%s (%s)", lang.Name, strings.Join(lang.Extensions, ", ")))
} }

View File

@ -151,7 +151,7 @@ func main() {
fmt.Println("Hello, World!") fmt.Println("Hello, World!")
} }
` `
if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil { if err := os.WriteFile(testFile, []byte(validCode), 0600); err != nil {
t.Fatalf("Failed to create test file: %v", err) t.Fatalf("Failed to create test file: %v", err)
} }
@ -207,7 +207,7 @@ func main() {
fmt.Println("Hello, World!") fmt.Println("Hello, World!")
} }
` `
if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil { if err := os.WriteFile(testFile, []byte(validCode), 0600); err != nil {
t.Fatalf("Failed to create test file: %v", err) t.Fatalf("Failed to create test file: %v", err)
} }
@ -237,7 +237,7 @@ func main() {
t.Run("Lint unsupported file type", func(t *testing.T) { t.Run("Lint unsupported file type", func(t *testing.T) {
testFile := filepath.Join(tmpDir, "test.txt") testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("hello"), 0644); err != nil { if err := os.WriteFile(testFile, []byte("hello"), 0600); err != nil {
t.Fatalf("Failed to create test file: %v", err) t.Fatalf("Failed to create test file: %v", err)
} }

View File

@ -21,12 +21,12 @@ func Init(logLevel string) error {
} }
logDir := filepath.Join(home, ".config", "grokkit") logDir := filepath.Join(home, ".config", "grokkit")
if err := os.MkdirAll(logDir, 0755); err != nil { if err := os.MkdirAll(logDir, 0750); err != nil {
return err return err
} }
logFile := filepath.Join(logDir, "grokkit.log") logFile := filepath.Join(logDir, "grokkit.log")
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil { if err != nil {
return err return err
} }

View File

@ -33,6 +33,7 @@ func TestVersionInfo(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tt.check(t) tt.check(t)
}) })
} }