Some checks failed
Beta Release / beta (push) Failing after 22s
- Add apikey step in onboarding wizard (optional, with validation) - Add ScanEditors() in scanner package detecting vim/nvim/code/emacs/nano/helix/subl/zed - Add GET /api/editors endpoint - Editor step now has scan button to detect installed editors via backend - MiniMax API key is saved to provider config if provided 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
269 lines
6.1 KiB
Go
269 lines
6.1 KiB
Go
package scanner
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/muyue/muyue/internal/platform"
|
|
)
|
|
|
|
type ToolStatus struct {
|
|
Name string `yaml:"name"`
|
|
Installed bool `yaml:"installed"`
|
|
Version string `yaml:"version"`
|
|
Path string `yaml:"path"`
|
|
Latest string `yaml:"latest"`
|
|
NeedsUpdate bool `yaml:"needs_update"`
|
|
Category string `yaml:"category"`
|
|
}
|
|
|
|
type RuntimeStatus struct {
|
|
Name string `yaml:"name"`
|
|
Installed bool `yaml:"installed"`
|
|
Version string `yaml:"version"`
|
|
}
|
|
|
|
type ScanResult struct {
|
|
System platform.SystemInfo `yaml:"system"`
|
|
Tools []ToolStatus `yaml:"tools"`
|
|
Runtimes []RuntimeStatus `yaml:"runtimes"`
|
|
ShellSetup bool `yaml:"shell_setup"`
|
|
GitConfigured bool `yaml:"git_configured"`
|
|
}
|
|
|
|
var (
|
|
cacheMu sync.RWMutex
|
|
cacheResult *ScanResult
|
|
cacheTime time.Time
|
|
cacheTTL = 5 * time.Minute
|
|
)
|
|
|
|
func ScanSystem() *ScanResult {
|
|
cacheMu.RLock()
|
|
if cacheResult != nil && time.Since(cacheTime) < cacheTTL {
|
|
result := cacheResult
|
|
cacheMu.RUnlock()
|
|
return result
|
|
}
|
|
cacheMu.RUnlock()
|
|
|
|
result := doScan()
|
|
|
|
cacheMu.Lock()
|
|
cacheResult = result
|
|
cacheTime = time.Now()
|
|
cacheMu.Unlock()
|
|
|
|
return result
|
|
}
|
|
|
|
func InvalidateCache() {
|
|
cacheMu.Lock()
|
|
cacheResult = nil
|
|
cacheTime = time.Time{}
|
|
cacheMu.Unlock()
|
|
}
|
|
|
|
func doScan() *ScanResult {
|
|
info := platform.Detect()
|
|
result := &ScanResult{
|
|
System: info,
|
|
}
|
|
|
|
result.Tools = scanTools()
|
|
result.Runtimes = scanRuntimes()
|
|
result.ShellSetup = checkShellSetup()
|
|
result.GitConfigured = checkGitConfig()
|
|
|
|
return result
|
|
}
|
|
|
|
func scanTools() []ToolStatus {
|
|
tools := []struct {
|
|
name string
|
|
category string
|
|
version []string
|
|
}{
|
|
{"crush", "ai", []string{"version", "--short"}},
|
|
{"claude", "ai", []string{"--version"}},
|
|
{"git", "vcs", []string{"--version"}},
|
|
{"node", "runtime", []string{"--version"}},
|
|
{"npm", "runtime", []string{"--version"}},
|
|
{"pnpm", "runtime", []string{"--version"}},
|
|
{"python3", "runtime", []string{"--version"}},
|
|
{"pip3", "runtime", []string{"--version"}},
|
|
{"uv", "runtime", []string{"--version"}},
|
|
{"go", "runtime", []string{"version"}},
|
|
{"docker", "devops", []string{"--version"}},
|
|
{"gh", "devops", []string{"--version"}},
|
|
{"starship", "prompt", []string{"--version"}},
|
|
{"npx", "runtime", []string{"--version"}},
|
|
}
|
|
|
|
var statuses []ToolStatus
|
|
for _, t := range tools {
|
|
status := ToolStatus{
|
|
Name: t.name,
|
|
Category: t.category,
|
|
}
|
|
|
|
path, err := exec.LookPath(t.name)
|
|
if err != nil {
|
|
statuses = append(statuses, status)
|
|
continue
|
|
}
|
|
|
|
status.Installed = true
|
|
status.Path = path
|
|
|
|
if len(t.version) > 0 {
|
|
cmd := exec.Command(t.name, t.version...)
|
|
out, err := cmd.Output()
|
|
if err == nil {
|
|
status.Version = strings.TrimSpace(string(out))
|
|
}
|
|
}
|
|
|
|
statuses = append(statuses, status)
|
|
}
|
|
|
|
return statuses
|
|
}
|
|
|
|
func scanRuntimes() []RuntimeStatus {
|
|
runtimes := []struct {
|
|
name string
|
|
command []string
|
|
}{
|
|
{"Go", []string{"go", "version"}},
|
|
{"Node.js", []string{"node", "--version"}},
|
|
{"Python", []string{"python3", "--version"}},
|
|
{"Rust", []string{"rustc", "--version"}},
|
|
{"Java", []string{"java", "--version"}},
|
|
{"Ruby", []string{"ruby", "--version"}},
|
|
{"PHP", []string{"php", "--version"}},
|
|
{"Dotnet", []string{"dotnet", "--version"}},
|
|
}
|
|
|
|
var statuses []RuntimeStatus
|
|
for _, r := range runtimes {
|
|
status := RuntimeStatus{Name: r.name}
|
|
cmd := exec.Command(r.command[0], r.command[1:]...)
|
|
out, err := cmd.Output()
|
|
if err == nil {
|
|
status.Installed = true
|
|
status.Version = strings.TrimSpace(string(out))
|
|
}
|
|
statuses = append(statuses, status)
|
|
}
|
|
|
|
return statuses
|
|
}
|
|
|
|
func checkShellSetup() bool {
|
|
home, _ := os.UserHomeDir()
|
|
rcFiles := []string{".bashrc", ".zshrc", ".config/fish/config.fish"}
|
|
for _, f := range rcFiles {
|
|
data, err := os.ReadFile(filepath.Join(home, f))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
content := string(data)
|
|
if strings.Contains(content, "starship") ||
|
|
strings.Contains(content, "muyue") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func checkGitConfig() bool {
|
|
for _, key := range []string{"user.name", "user.email"} {
|
|
cmd := exec.Command("git", "config", "--global", key)
|
|
if _, err := cmd.Output(); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
var editorsList = []struct {
|
|
name string
|
|
cmd []string
|
|
version []string
|
|
}{
|
|
{"vim", []string{"vim"}, []string{"--version"}},
|
|
{"nvim", []string{"nvim"}, []string{"--version"}},
|
|
{"code", []string{"code"}, []string{"--version"}},
|
|
{"emacs", []string{"emacs"}, []string{"--version"}},
|
|
{"nano", []string{"nano"}, []string{"--version"}},
|
|
{"helix", []string{"hx"}, []string{"--version"}},
|
|
{"subl", []string{"subl"}, []string{"--version"}},
|
|
{"zed", []string{"zed"}, []string{"--version"}},
|
|
}
|
|
|
|
func ScanEditors() []ToolStatus {
|
|
var results []ToolStatus
|
|
for _, e := range editorsList {
|
|
status := ToolStatus{Name: e.name}
|
|
path, err := exec.LookPath(e.name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
status.Installed = true
|
|
status.Path = path
|
|
if len(e.version) > 0 {
|
|
cmd := exec.Command(e.cmd[0], e.version...)
|
|
out, err := cmd.Output()
|
|
if err == nil {
|
|
status.Version = strings.TrimSpace(string(out))
|
|
}
|
|
}
|
|
results = append(results, status)
|
|
}
|
|
return results
|
|
}
|
|
|
|
var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`)
|
|
|
|
func (s *ScanResult) Summary() string {
|
|
var b strings.Builder
|
|
|
|
fmt.Fprintf(&b, "System: %s\n", s.System.String())
|
|
fmt.Fprintf(&b, "\nTools:\n")
|
|
|
|
installed := 0
|
|
for _, t := range s.Tools {
|
|
if t.Installed {
|
|
installed++
|
|
fmt.Fprintf(&b, " [v] %s %s\n", t.Name, versionRegex.FindString(t.Version))
|
|
} else {
|
|
fmt.Fprintf(&b, " [ ] %s (not installed)\n", t.Name)
|
|
}
|
|
}
|
|
fmt.Fprintf(&b, "\nInstalled: %d/%d\n", installed, len(s.Tools))
|
|
|
|
fmt.Fprintf(&b, "\nRuntimes:\n")
|
|
for _, r := range s.Runtimes {
|
|
if r.Installed {
|
|
fmt.Fprintf(&b, " [v] %s\n", r.Version)
|
|
} else {
|
|
fmt.Fprintf(&b, " [ ] %s (not installed)\n", r.Name)
|
|
}
|
|
}
|
|
|
|
if s.GitConfigured {
|
|
fmt.Fprintf(&b, "\nGit: configured\n")
|
|
} else {
|
|
fmt.Fprintf(&b, "\nGit: not fully configured\n")
|
|
}
|
|
|
|
return b.String()
|
|
}
|