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>
174 lines
3.1 KiB
Go
174 lines
3.1 KiB
Go
package daemon
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/muyue/muyue/internal/config"
|
|
"github.com/muyue/muyue/internal/scanner"
|
|
"github.com/muyue/muyue/internal/updater"
|
|
)
|
|
|
|
type Daemon struct {
|
|
config *config.MuyueConfig
|
|
interval time.Duration
|
|
stopCh chan struct{}
|
|
mu sync.RWMutex
|
|
running bool
|
|
lastCheck time.Time
|
|
lastStatus []updater.UpdateStatus
|
|
logs []string
|
|
onUpdate func([]updater.UpdateStatus)
|
|
}
|
|
|
|
func NewDaemon(cfg *config.MuyueConfig, interval time.Duration) *Daemon {
|
|
if interval == 0 {
|
|
interval = 1 * time.Hour
|
|
}
|
|
return &Daemon{
|
|
config: cfg,
|
|
interval: interval,
|
|
stopCh: make(chan struct{}),
|
|
logs: []string{},
|
|
}
|
|
}
|
|
|
|
func (d *Daemon) OnUpdate(fn func([]updater.UpdateStatus)) {
|
|
d.onUpdate = fn
|
|
}
|
|
|
|
func (d *Daemon) Start() error {
|
|
d.mu.Lock()
|
|
if d.running {
|
|
d.mu.Unlock()
|
|
return fmt.Errorf("daemon already running")
|
|
}
|
|
d.running = true
|
|
d.mu.Unlock()
|
|
|
|
d.log("daemon started (interval: %s)", d.interval)
|
|
|
|
go d.run()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Daemon) Stop() {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if !d.running {
|
|
return
|
|
}
|
|
d.running = false
|
|
d.stopCh <- struct{}{}
|
|
d.log("daemon stopped")
|
|
}
|
|
|
|
func (d *Daemon) IsRunning() bool {
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
return d.running
|
|
}
|
|
|
|
func (d *Daemon) LastCheck() time.Time {
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
return d.lastCheck
|
|
}
|
|
|
|
func (d *Daemon) LastStatus() []updater.UpdateStatus {
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
return d.lastStatus
|
|
}
|
|
|
|
func (d *Daemon) Logs() []string {
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
return d.logs
|
|
}
|
|
|
|
func (d *Daemon) TriggerCheck() []updater.UpdateStatus {
|
|
return d.checkUpdates()
|
|
}
|
|
|
|
func (d *Daemon) run() {
|
|
d.checkUpdates()
|
|
|
|
ticker := time.NewTicker(d.interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
d.checkUpdates()
|
|
case <-d.stopCh:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Daemon) checkUpdates() []updater.UpdateStatus {
|
|
d.log("checking for updates...")
|
|
result := scanner.ScanSystem()
|
|
statuses := updater.CheckUpdates(result)
|
|
|
|
needsUpdate := false
|
|
for _, s := range statuses {
|
|
if s.NeedsUpdate {
|
|
needsUpdate = true
|
|
d.log("update available: %s %s -> %s", s.Tool, s.Current, s.Latest)
|
|
}
|
|
}
|
|
|
|
if !needsUpdate {
|
|
d.log("all tools up to date")
|
|
}
|
|
|
|
d.mu.Lock()
|
|
d.lastCheck = time.Now()
|
|
d.lastStatus = statuses
|
|
d.mu.Unlock()
|
|
|
|
if d.config.Profile.Preferences.AutoUpdate && needsUpdate {
|
|
d.log("auto-updating...")
|
|
results := updater.RunAutoUpdate(statuses)
|
|
for _, r := range results {
|
|
if r.Message != "" {
|
|
d.log(" %s: %s", r.Tool, r.Message)
|
|
}
|
|
}
|
|
}
|
|
|
|
if d.onUpdate != nil {
|
|
d.onUpdate(statuses)
|
|
}
|
|
|
|
return statuses
|
|
}
|
|
|
|
func (d *Daemon) log(format string, args ...interface{}) {
|
|
msg := fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05"), fmt.Sprintf(format, args...))
|
|
d.mu.Lock()
|
|
d.logs = append(d.logs, msg)
|
|
if len(d.logs) > 500 {
|
|
d.logs = d.logs[250:]
|
|
}
|
|
d.mu.Unlock()
|
|
}
|
|
|
|
func RunStandalone(cfg *config.MuyueConfig) {
|
|
d := NewDaemon(cfg, 1*time.Hour)
|
|
d.Start()
|
|
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
<-sigCh
|
|
d.Stop()
|
|
}
|