feat: security hardening, tests, doctor command, CI update, CHANGELOG
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>
This commit is contained in:
Augustin
2026-04-20 19:56:07 +02:00
parent 44691225e7
commit 3494f6b40d
22 changed files with 1655 additions and 253 deletions

View File

@@ -6,6 +6,8 @@ import (
"os/exec"
"regexp"
"strings"
"sync"
"time"
"github.com/muyue/muyue/internal/platform"
)
@@ -34,7 +36,40 @@ type ScanResult struct {
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,

View File

@@ -0,0 +1,76 @@
package scanner
import (
"strings"
"testing"
)
func TestScanSystem(t *testing.T) {
InvalidateCache()
result := ScanSystem()
if result == nil {
t.Fatal("ScanSystem should not return nil")
}
if result.System.OS == "" {
t.Error("System OS should not be empty")
}
}
func TestScanTools(t *testing.T) {
tools := scanTools()
if len(tools) == 0 {
t.Error("Should scan at least some tools")
}
for _, tool := range tools {
if tool.Name == "" {
t.Error("Tool name should not be empty")
}
}
}
func TestScanRuntimes(t *testing.T) {
runtimes := scanRuntimes()
if len(runtimes) == 0 {
t.Error("Should scan at least some runtimes")
}
for _, r := range runtimes {
if r.Name == "" {
t.Error("Runtime name should not be empty")
}
}
}
func TestCheckGitConfig(t *testing.T) {
_ = checkGitConfig()
}
func TestCheckShellSetup(t *testing.T) {
_ = checkShellSetup()
}
func TestSummary(t *testing.T) {
InvalidateCache()
result := ScanSystem()
summary := result.Summary()
if summary == "" {
t.Error("Summary should not be empty")
}
if !strings.Contains(summary, "System:") {
t.Error("Summary should contain System:")
}
if !strings.Contains(summary, "Tools:") {
t.Error("Summary should contain Tools:")
}
if !strings.Contains(summary, "Runtimes:") {
t.Error("Summary should contain Runtimes:")
}
}
func TestScanCache(t *testing.T) {
InvalidateCache()
r1 := ScanSystem()
r2 := ScanSystem()
if r1 != r2 {
t.Error("Cached result should be the same pointer")
}
}