Files
MuyueWorkspace/internal/scanner/scanner.go
Augustin 04b0fff791
All checks were successful
Beta Release / beta (push) Successful in 44s
refactor(api): split monolithic handlers.go into focused modules
Break down the 627-line handlers.go into specialized modules:
- handlers_chat.go: chat and streaming endpoints
- handlers_config.go: configuration endpoints
- handlers_common.go: shared utilities
- handlers_info.go: info and status endpoints
- handlers_terminal.go: terminal/shell endpoints
- handlers_tools.go: tool-related endpoints

Also includes config improvements, orchestrator enhancements, and
web component updates.

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
2026-04-22 18:34:14 +02:00

232 lines
5.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 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()
}