package updater import ( "encoding/json" "fmt" "net/http" "os/exec" "regexp" "strings" "time" "github.com/muyue/muyue/internal/scanner" ) type UpdateStatus struct { Tool string `json:"tool"` Current string `json:"current"` Latest string `json:"latest"` NeedsUpdate bool `json:"needs_update"` Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` } var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`) var sharedHTTPClient = &http.Client{Timeout: 10 * time.Second} type githubRelease struct { TagName string `json:"tag_name"` } func CheckUpdates(result *scanner.ScanResult) []UpdateStatus { var statuses []UpdateStatus for _, tool := range result.Tools { if !tool.Installed { continue } status := UpdateStatus{ Tool: tool.Name, Current: versionRegex.FindString(tool.Version), } latest, err := getLatestVersion(tool.Name) if err != nil { status.Error = err.Error() statuses = append(statuses, status) continue } status.Latest = latest status.NeedsUpdate = status.Current != "" && status.Current != latest statuses = append(statuses, status) } return statuses } func getLatestVersion(tool string) (string, error) { repos := map[string]string{ "crush": "charmbracelet/crush", "gh": "cli/cli", "starship": "starship/starship", "docker": "moby/moby", } repo, ok := repos[tool] if !ok { return getLatestVersionCLI(tool) } url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo) resp, err := sharedHTTPClient.Get(url) if err != nil { return "", fmt.Errorf("github api: %w", err) } defer resp.Body.Close() var release githubRelease if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { return "", fmt.Errorf("decode: %w", err) } return strings.TrimPrefix(release.TagName, "v"), nil } func getLatestVersionCLI(tool string) (string, error) { commands := map[string][]string{ "node": {"node", "--version"}, "npm": {"npm", "--version"}, "go": {"go", "version"}, "python3": {"python3", "--version"}, "git": {"git", "--version"}, "claude": {"claude", "--version"}, } cmd, ok := commands[tool] if !ok { return "", fmt.Errorf("no update check for %s", tool) } out, err := exec.Command(cmd[0], cmd[1:]...).Output() if err != nil { return "", err } v := versionRegex.FindString(string(out)) if v == "" { return strings.TrimSpace(string(out)), nil } return v, nil } func RunAutoUpdate(statuses []UpdateStatus) []UpdateStatus { var results []UpdateStatus for _, s := range statuses { if !s.NeedsUpdate || s.Error != "" { results = append(results, s) continue } switch s.Tool { case "crush": err := runCommand("bash", "-c", "curl -fsSL https://github.com/charmbracelet/crush/releases/latest/download/install.sh | bash") if err != nil { s.Error = err.Error() } else { s.NeedsUpdate = false s.Message = "updated" } case "claude": err := runCommand("npm", "update", "-g", "@anthropic-ai/claude-code") if err != nil { s.Error = err.Error() } else { s.NeedsUpdate = false s.Message = "updated" } case "starship": err := runCommand("bash", "-c", "curl -sS https://starship.rs/install.sh | sh -s -- -y") if err != nil { s.Error = err.Error() } else { s.NeedsUpdate = false s.Message = "updated" } default: s.Error = "auto-update not supported" } results = append(results, s) } return results } func runCommand(name string, args ...string) error { cmd := exec.Command(name, args...) return cmd.Run() }