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" json:"name"` Installed bool `yaml:"installed" json:"installed"` Version string `yaml:"version" json:"version"` Path string `yaml:"path" json:"path"` Latest string `yaml:"latest" json:"latest"` NeedsUpdate bool `yaml:"needs_update" json:"needs_update"` Category string `yaml:"category" json:"category"` } type RuntimeStatus struct { Name string `yaml:"name" json:"name"` Installed bool `yaml:"installed" json:"installed"` Version string `yaml:"version" json:"version"` } type ScanResult struct { System platform.SystemInfo `yaml:"system" json:"system"` Tools []ToolStatus `yaml:"tools" json:"tools"` Runtimes []RuntimeStatus `yaml:"runtimes" json:"runtimes"` ShellSetup bool `yaml:"shell_setup" json:"shell_setup"` GitConfigured bool `yaml:"git_configured" json:"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() }