All checks were successful
CI / build (push) Successful in 2m37s
- Add AES-256-GCM encryption for API keys (internal/secret) - Add dangerous command detection in terminal - Add muyue doctor command for system health checks - Add scanner TTL cache, orchestrator history mutex, shared HTTP client - Deduplicate MCP config generation, refactor skills YAML parser - Add XDG-compliant config dir with legacy migration - Add cleanup on all TUI quit paths - Add 8 test files (config, workflow, skills, orchestrator, version, platform, scanner, secret) - Update CI to actions/setup-go@v5 - Add CHANGELOG.md, update README and Makefile 🤖 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
164 lines
3.6 KiB
Go
164 lines
3.6 KiB
Go
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()
|
|
}
|