Files
MuyueWorkspace/internal/scanner/scanner.go
Augustin bc5c2956b4
Some checks failed
Beta Release / beta (push) Failing after 22s
feat(onboarding): add minimax api key step and AI-powered editor scan
- 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>
2026-04-22 21:04:27 +02:00

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()
}