diff --git a/src/cli/spinner.go b/src/cli/spinner.go new file mode 100644 index 0000000..4f143d3 --- /dev/null +++ b/src/cli/spinner.go @@ -0,0 +1,21 @@ +package cli + +import ( + "fmt" + + "time" + + "github.com/briandowns/spinner" +) + +func RunWithSpinner(message string, task func() error) error { + s := spinner.New(spinner.CharSets[11], 100*time.Millisecond) + s.Suffix = fmt.Sprintf(" %s", message) + s.Start() + + err := task() + + s.Stop() + + return err +} diff --git a/src/cmd/fix.go b/src/cmd/fix.go deleted file mode 100644 index e9c6ec3..0000000 --- a/src/cmd/fix.go +++ /dev/null @@ -1,135 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "github.com/spf13/cobra" - "os" - "path/filepath" - - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" -) - -var selectedFiles = make(map[string]bool) - -func newFixCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "fix", - Short: "Detect and fix issues in a project", - Long: `An interactive tool to scan project files, identify potential problems, and suggest fixes.`, - RunE: func(_ *cobra.Command, args []string) error { - path := "." - if len(args) > 0 { - path = args[0] - } - - if _, err := os.Stat(path); os.IsNotExist(err) { - return errors.New("the specified path does not exist") - } - - app := tview.NewApplication() - treeRoot := tview.NewTreeNode(path).SetSelectable(false) - treeView := tview.NewTreeView().SetRoot(treeRoot).SetCurrentNode(treeRoot) - - loadFilesIntoTree(treeRoot, path) - - treeView.SetSelectedFunc(func(node *tview.TreeNode) { - path, ok := node.GetReference().(string) - if !ok { - return - } - - if selectedFiles[path] { - deselectRecursively(node) - } else { - selectRecursively(node) - } - }) - - treeView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyEnter { - app.Stop() - printSelectedFiles() - return nil - } - return event - }) - - layout := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(tview.NewTextView().SetText("Use ↑↓ to navigate, Space to select, Enter to confirm."), 1, 1, false). - AddItem(treeView, 0, 1, true) - - if err := app.SetRoot(layout, true).Run(); err != nil { - return err - } - - return nil - }, - } - - return cmd -} - -func loadFilesIntoTree(parentNode *tview.TreeNode, path string) { - files, err := os.ReadDir(path) - if err != nil { - return - } - - for _, file := range files { - filePath := filepath.Join(path, file.Name()) - node := tview.NewTreeNode(file.Name()). - SetReference(filePath). - SetSelectable(true) - - if file.IsDir() { - node.SetColor(tcell.ColorBlue) - loadFilesIntoTree(node, filePath) - } else { - node.SetColor(tcell.ColorWhite) - } - - parentNode.AddChild(node) - } -} - -func selectRecursively(node *tview.TreeNode) { - path, ok := node.GetReference().(string) - if !ok { - return - } - - selectedFiles[path] = true - node.SetColor(tcell.ColorGreen) - - for _, child := range node.GetChildren() { - selectRecursively(child) - } -} - -func deselectRecursively(node *tview.TreeNode) { - path, ok := node.GetReference().(string) - if !ok { - return - } - - delete(selectedFiles, path) - node.SetColor(tcell.ColorWhite) - - for _, child := range node.GetChildren() { - deselectRecursively(child) - } -} - -func printSelectedFiles() { - if len(selectedFiles) == 0 { - fmt.Println("No files selected.") - return - } - fmt.Println("Selected files:") - for file := range selectedFiles { - fmt.Println(file) - } -} diff --git a/src/cmd/root.go b/src/cmd/root.go index 92a6387..a92af2f 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -4,7 +4,7 @@ import ( "github.com/spf13/cobra" ) -func NewRootCmd() *cobra.Command { +func RootCmd() *cobra.Command { cmd := &cobra.Command{ Use: "trood", Short: "A CLI tool for detecting and fixing issues in your projects.", @@ -13,7 +13,7 @@ identify potential issues, and provide actionable fixes.`, CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, } - cmd.AddCommand(newFixCmd()) + cmd.AddCommand(ScanCmd()) return cmd } diff --git a/src/cmd/scan.go b/src/cmd/scan.go new file mode 100644 index 0000000..16c7343 --- /dev/null +++ b/src/cmd/scan.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/troodinc/trood/internal/scanner/node" +) + +func ScanCmd() *cobra.Command { + var path string + + cmd := &cobra.Command{ + Use: "scan", + Short: "Scan a project for dependency issues.", + Long: `The scan command will check a Node.js project for: + - Missing dependencies + - Unused dependencies + - Security vulnerabilities`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("please provide the path to scan") + } + path := args[0] + return node.RunNodeScan(path) + }, + } + + cmd.Flags().StringVarP(&path, "path", "p", "", "Path to the project to scan (defaults to current directory)") + + return cmd +} diff --git a/src/go.mod b/src/go.mod index e9375ea..dff7a83 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,10 +3,14 @@ module github.com/troodinc/trood go 1.23.2 require ( + github.com/briandowns/spinner v1.23.2 // indirect + github.com/fatih/color v1.7.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/gdamore/tcell/v2 v2.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.8 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57 // indirect github.com/rivo/uniseg v0.4.7 // indirect diff --git a/src/go.sum b/src/go.sum index ac618e8..12febd6 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,4 +1,8 @@ +github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= +github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= @@ -12,6 +16,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= @@ -54,6 +62,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/src/internal/scanner/detect.go b/src/internal/scanner/detect.go new file mode 100644 index 0000000..9de1578 --- /dev/null +++ b/src/internal/scanner/detect.go @@ -0,0 +1,11 @@ +package scanner + +import "github.com/troodinc/trood/internal/scanner/node" + +func DetectEnvironment() string { + if node.IsNodeProject() { + return "nodejs" + } + + return "unknown" +} diff --git a/src/internal/scanner/node/dependencies.go b/src/internal/scanner/node/dependencies.go new file mode 100644 index 0000000..b0a6326 --- /dev/null +++ b/src/internal/scanner/node/dependencies.go @@ -0,0 +1,50 @@ +package node + +import ( + "errors" + "fmt" + "os/exec" + "strings" +) + +func CheckMissingDependencies(path string) error { + cmd := exec.Command("npm", "install", "--dry-run") + cmd.Dir = path + output, _ := cmd.CombinedOutput() + + if strings.Contains(string(output), "added") { + fmt.Println("\n\n⚠️ Missing dependencies detected") + fmt.Println(string(output)) + } else { + fmt.Printf("✅ No missing dependencies found.") + } + + return nil +} + +func CheckUnusedDependencies(path string) error { + cmd := exec.Command("npx", "depcheck") + cmd.Dir = path + output, _ := cmd.CombinedOutput() + + if len(output) > 0 { + return errors.New(string(output)) + } + + return nil +} + +func CheckSecurityIssues(path string) error { + cmd := exec.Command("npm", "audit", "--json") + cmd.Dir = path + output, _ := cmd.CombinedOutput() + + if len(output) > 0 { + fmt.Println("\n\n📢 Security vulnerabilities detected") + fmt.Println(string(output)) + } else { + fmt.Println("✅ No security vulnerabilities found.") + } + + return nil +} diff --git a/src/internal/scanner/node/detect.go b/src/internal/scanner/node/detect.go new file mode 100644 index 0000000..380c62a --- /dev/null +++ b/src/internal/scanner/node/detect.go @@ -0,0 +1,8 @@ +package node + +import "os" + +func IsNodeProject() bool { + _, err := os.Stat("package.json") + return err == nil +} diff --git a/src/internal/scanner/node/scan.go b/src/internal/scanner/node/scan.go new file mode 100644 index 0000000..f9f4fd0 --- /dev/null +++ b/src/internal/scanner/node/scan.go @@ -0,0 +1,21 @@ +package node + +import ( + "github.com/troodinc/trood/cli" +) + +func RunNodeScan(path string) error { + cli.RunWithSpinner("Checking for missing dependencies", func() error { + return CheckMissingDependencies(path) + }) + + cli.RunWithSpinner("Checking for unused dependencies", func() error { + return CheckUnusedDependencies(path) + }) + + cli.RunWithSpinner("Checking for security vulnerabilities", func() error { + return CheckSecurityIssues(path) + }) + + return nil +} diff --git a/src/main.go b/src/main.go index ee7d24c..e32749d 100644 --- a/src/main.go +++ b/src/main.go @@ -2,18 +2,19 @@ package main import ( "context" - "github.com/troodinc/trood/cmd" "log" "os" "os/signal" "syscall" + + "github.com/troodinc/trood/cmd" ) func main() { ctx, shutdown := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer shutdown() - root := cmd.NewRootCmd() + root := cmd.RootCmd() if err := root.ExecuteContext(ctx); err != nil { log.Fatal(err) }