Files
MuyueWorkspace/internal/scanner/scanner.go
Augustin f0ccd265da
Some checks failed
CI / build (macos-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
feat: initial release of muyue - AI-powered dev environment assistant
Complete implementation of muyue v0.1.0, a single-binary Go tool that
transforms the development environment with AI-powered orchestration.

Core features:
- TUI with 5 tabs (Dashboard/Chat/Workflow/Agents/Config) using Charm stack
- AI chat via MiniMax M2.7 with async message handling
- Structured Plan→Execute workflow engine (gather→plan→review→execute)
- System scanner detecting 14 tools + 8 runtimes across Linux/macOS/Windows
- Auto-installer for Crush, Claude Code, BMAD, Starship, runtimes
- Background update daemon with hourly checks
- LSP auto-config for 16 language servers
- MCP auto-config for 12 servers (deployed to Crush + Claude Code)
- Skills system with 5 built-ins + AI-powered generation
- Crush/Claude Code proxy for unified control
- HTML preview server for visual outputs
- First-time setup wizard with interactive profiling
- Cross-platform: Linux (primary), macOS, Windows, WSL

CI/CD:
- GitHub Actions CI: build + test + lint on Linux/macOS/Windows
- Release workflow: cross-compile 6 binaries with checksums on tag push

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-19 22:29:20 +02:00

196 lines
4.5 KiB
Go

package scanner
import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"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"`
}
func ScanSystem() *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(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()
}