feat: initial release of muyue - AI-powered dev environment assistant
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>
This commit is contained in:
173
internal/daemon/daemon.go
Normal file
173
internal/daemon/daemon.go
Normal file
@@ -0,0 +1,173 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user