Files
Augustin 3494f6b40d
All checks were successful
CI / build (push) Successful in 2m37s
feat: security hardening, tests, doctor command, CI update, CHANGELOG
- 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>
2026-04-20 19:56:07 +02:00

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