Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ jobs:
cache: true

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v7
with:
version: latest
version: v2.10.1
args: --timeout=5m

build:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ jobs:
cache: true

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v7
with:
version: latest
version: v2.10.1
args: --timeout=5m

build:
Expand Down
72 changes: 31 additions & 41 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,59 +1,49 @@
version: "2"

run:
timeout: 5m
tests: true
modules-download-mode: readonly

linters:
default: standard
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- revive
- gosec
- unconvert
- goconst
- gocyclo
- dupl

linters-settings:
errcheck:
check-type-assertions: true
check-blank: true

govet:
enable-all: true
disable:
- shadow

gocyclo:
min-complexity: 15

dupl:
threshold: 100

goconst:
min-len: 3
min-occurrences: 3

misspell:
locale: US

revive:
confidence: 0.8
settings:
errcheck:
check-type-assertions: true
check-blank: true
gocyclo:
min-complexity: 15
dupl:
threshold: 100
goconst:
min-len: 3
min-occurrences: 3
misspell:
locale: US
revive:
confidence: 0.8
exclusions:
rules:
- path: _test\.go
linters:
- gocyclo
- errcheck
- dupl
- gosec

formatters:
enable:
- gofmt
- goimports

issues:
max-issues-per-linter: 0
max-same-issues: 0
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- errcheck
- dupl
- gosec
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ module pumu

go 1.24.0

require github.com/fatih/color v1.18.0
require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/fatih/color v1.18.0
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/analyzer.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package pkg provides utilities for detecting, installing, checking,
// and analyzing dependencies across multiple package managers.
package pkg

import (
Expand Down
16 changes: 3 additions & 13 deletions internal/pkg/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package pkg
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
Expand Down Expand Up @@ -51,7 +50,7 @@ func checkNodeHealth(dir string, pm PackageManager, binary string) HealthResult
result := HealthResult{Dir: dir, PM: pm, Healthy: true}

targetPath := dir + "/node_modules"
if !fileExists(targetPath) && !dirExists(targetPath) {
if !FileExists(targetPath) && !DirExists(targetPath) {
result.Healthy = false
result.Issues = append(result.Issues, "node_modules not found")
return result
Expand Down Expand Up @@ -120,7 +119,7 @@ func checkCargoHealth(dir string) HealthResult {
result := HealthResult{Dir: dir, PM: Cargo, Healthy: true}

targetPath := dir + "/target"
if !dirExists(targetPath) {
if !DirExists(targetPath) {
result.Healthy = false
result.Issues = append(result.Issues, "target/ not found (never built)")
return result
Expand Down Expand Up @@ -182,7 +181,7 @@ func checkPipHealth(dir string) HealthResult {
result := HealthResult{Dir: dir, PM: Pip, Healthy: true}

venvPath := dir + "/.venv"
if !dirExists(venvPath) {
if !DirExists(venvPath) {
result.Healthy = false
result.Issues = append(result.Issues, ".venv not found")
return result
Expand Down Expand Up @@ -213,12 +212,3 @@ func checkPipHealth(dir string) HealthResult {

return result
}

// dirExists checks if a directory exists at the given path.
func dirExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}
5 changes: 5 additions & 0 deletions internal/pkg/detector.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// Package pkg provides utilities for detecting, installing, checking,
// and analyzing dependencies across multiple package managers.
package pkg

import (
"os"
"path/filepath"
)

// PackageManager represents the type of package manager detected in a project.
type PackageManager string

// Supported package managers.
const (
Npm PackageManager = "npm"
Pnpm PackageManager = "pnpm"
Expand All @@ -19,6 +23,7 @@ const (
Unknown PackageManager = "unknown"
)

// DetectManager identifies the package manager used in dir by checking for lock files.
func DetectManager(dir string) PackageManager {
managers := map[PackageManager][]string{
Bun: {"bun.lockb", "bun.lock"},
Expand Down
10 changes: 6 additions & 4 deletions internal/pkg/detector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestDetectManager(t *testing.T) {
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
defer func() { _ = os.RemoveAll(tempDir) }()

tests := []struct {
name string
Expand All @@ -33,17 +33,19 @@ func TestDetectManager(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
caseDir := filepath.Join(tempDir, tt.name)
err := os.MkdirAll(caseDir, 0755)
err := os.MkdirAll(caseDir, 0o750) //nolint:gosec // test directory
if err != nil {
t.Fatalf("failed to create dir %s: %v", caseDir, err)
}

lockfilePath := filepath.Join(caseDir, tt.lockfileName)
file, err := os.Create(lockfilePath)
file, err := os.Create(lockfilePath) //nolint:gosec // controlled test path
if err != nil {
t.Fatalf("failed to create fake lock file %s: %v", lockfilePath, err)
}
file.Close()
if err := file.Close(); err != nil {
t.Fatalf("failed to close file: %v", err)
}

pm := DetectManager(caseDir)
if pm != tt.expected {
Expand Down
21 changes: 21 additions & 0 deletions internal/pkg/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package pkg

import "os"

// FileExists reports whether a path exists and is a regular file (not a directory).
func FileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}

// DirExists reports whether a path exists and is a directory.
func DirExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}
44 changes: 13 additions & 31 deletions internal/scanner/repair.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"sync"
"strings"

"pumu/internal/pkg"

Expand All @@ -13,7 +13,7 @@ import (

// RepairDir scans for projects with broken dependencies and repairs them.
func RepairDir(root string, verbose bool) error {
color.Cyan(" Scanning for projects with broken dependencies in '%s'...\n", root)
color.Cyan("🔧 Scanning for projects with broken dependencies in '%s'...\n", root)

projects, err := findProjects(root)
if err != nil {
Expand All @@ -25,7 +25,7 @@ func RepairDir(root string, verbose bool) error {
return nil
}

color.Yellow("👽 Found %d projects. Checking health...\n", len(projects))
color.Yellow("⏱️ Found %d projects. Checking health...\n", len(projects))

var repaired, total int

Expand All @@ -36,7 +36,7 @@ func RepairDir(root string, verbose bool) error {
if result.Healthy {
if verbose {
fmt.Printf("\n📁 %s (%s)\n", proj.Dir, proj.PM)
color.Green(" 😎 Healthy, skipping.")
color.Green(" Healthy, skipping.")
}
continue
}
Expand All @@ -51,8 +51,8 @@ func RepairDir(root string, verbose bool) error {
targetFolder := getTargetFolder(proj.PM)
targetPath := filepath.Join(proj.Dir, targetFolder)

if dirExists(targetPath) {
fmt.Printf(" 🤢🗑️ Removing %s...\n", targetFolder)
if pkg.DirExists(targetPath) {
fmt.Printf(" 🗑️ Removing %s...\n", targetFolder)
_, err := pkg.RemoveDirectory(targetPath)
if err != nil {
color.Red(" ❌ Failed to remove %s: %v", targetFolder, err)
Expand All @@ -61,20 +61,20 @@ func RepairDir(root string, verbose bool) error {
}

// Reinstall
fmt.Printf(" 🏃🏾‍♂️‍➡️ Reinstalling...\n")
fmt.Printf(" 📦 Reinstalling...\n")
err := pkg.InstallDependencies(proj.Dir, proj.PM, true)
if err != nil {
color.Red(" 😢 Failed to reinstall: %v", err)
color.Red(" Failed to reinstall: %v", err)
continue
}

color.Green(" 😎 Repaired!")
color.Green(" Repaired!")
repaired++
}

fmt.Println()
fmt.Println("-----")
color.Green("👽 Repair complete! Fixed %d/%d projects.", repaired, total)
fmt.Println(strings.Repeat("-", 40))
color.Green("🔧 Repair complete! Fixed %d/%d projects.", repaired, total)

return nil
}
Expand All @@ -86,12 +86,9 @@ type project struct {
}

// findProjects recursively scans for directories containing lockfiles/manifests.
// WalkDir is sequential, so no mutex is needed.
func findProjects(root string) ([]project, error) {
var projects []project
var mu sync.Mutex

// Track visited dirs to avoid duplicates
visited := make(map[string]bool)

err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
Expand All @@ -103,15 +100,9 @@ func findProjects(root string) ([]project, error) {
return filepath.SkipDir
}

// Try to detect package manager
pm := pkg.DetectManager(path)
if pm != pkg.Unknown {
mu.Lock()
if !visited[path] {
visited[path] = true
projects = append(projects, project{Dir: path, PM: pm})
}
mu.Unlock()
projects = append(projects, project{Dir: path, PM: pm})
}
}

Expand All @@ -120,12 +111,3 @@ func findProjects(root string) ([]project, error) {

return projects, err
}

// dirExists checks if a directory exists at the given path.
func dirExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}
Loading
Loading