package api import ( "encoding/json" "net/http" "os/exec" "strings" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/lsp" "github.com/muyue/muyue/internal/mcp" "github.com/muyue/muyue/internal/scanner" "github.com/muyue/muyue/internal/skills" "github.com/muyue/muyue/internal/updater" "github.com/muyue/muyue/internal/version" ) type Server struct { config *config.MuyueConfig scanResult *scanner.ScanResult mux *http.ServeMux } func NewServer(cfg *config.MuyueConfig) *Server { s := &Server{ config: cfg, mux: http.NewServeMux(), } s.scanResult = scanner.ScanSystem() s.routes() return s } func (s *Server) routes() { s.mux.HandleFunc("/api/info", s.handleInfo) s.mux.HandleFunc("/api/system", s.handleSystem) s.mux.HandleFunc("/api/tools", s.handleTools) s.mux.HandleFunc("/api/config", s.handleConfig) s.mux.HandleFunc("/api/providers", s.handleProviders) s.mux.HandleFunc("/api/skills", s.handleSkills) s.mux.HandleFunc("/api/lsp", s.handleLSP) s.mux.HandleFunc("/api/mcp", s.handleMCP) s.mux.HandleFunc("/api/updates", s.handleUpdates) s.mux.HandleFunc("/api/install", s.handleInstall) s.mux.HandleFunc("/api/scan", s.handleScan) s.mux.HandleFunc("/api/terminal", s.handleTerminal) s.mux.HandleFunc("/api/mcp/configure", s.handleMCPConfigure) } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } s.mux.ServeHTTP(w, r) } func writeJSON(w http.ResponseWriter, data interface{}) { json.NewEncoder(w).Encode(data) } func writeError(w http.ResponseWriter, msg string, code int) { w.WriteHeader(code) json.NewEncoder(w).Encode(map[string]string{"error": msg}) } func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]interface{}{ "name": version.Name, "version": version.Version, "author": version.Author, }) } func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) { if s.scanResult == nil { s.scanResult = scanner.ScanSystem() } writeJSON(w, map[string]interface{}{ "system": s.scanResult.System, }) } type ToolInfo struct { Name string `json:"name"` Installed bool `json:"installed"` Version string `json:"version"` Path string `json:"path"` } func (s *Server) handleTools(w http.ResponseWriter, r *http.Request) { if s.scanResult == nil { s.scanResult = scanner.ScanSystem() } tools := make([]ToolInfo, len(s.scanResult.Tools)) for i, t := range s.scanResult.Tools { tools[i] = ToolInfo{ Name: t.Name, Installed: t.Installed, Version: t.Version, Path: t.Path, } } writeJSON(w, map[string]interface{}{ "tools": tools, "total": len(tools), }) } func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { if s.config == nil { writeError(w, "no config", http.StatusNotFound) return } writeJSON(w, map[string]interface{}{ "profile": s.config.Profile, "terminal": s.config.Terminal, "bmad": s.config.BMAD, }) } func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) { if s.config == nil { writeError(w, "no config", http.StatusNotFound) return } writeJSON(w, map[string]interface{}{ "providers": s.config.AI.Providers, }) } func (s *Server) handleSkills(w http.ResponseWriter, r *http.Request) { list, err := skills.List() if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]interface{}{ "skills": list, "count": len(list), }) } func (s *Server) handleLSP(w http.ResponseWriter, r *http.Request) { servers := lsp.ScanServers() writeJSON(w, map[string]interface{}{ "servers": servers, }) } func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) { servers := mcp.ScanServers() writeJSON(w, map[string]interface{}{ "servers": servers, "configured": true, }) } func (s *Server) handleMCPConfigure(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } err := mcp.ConfigureAll(s.config) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } func (s *Server) handleUpdates(w http.ResponseWriter, r *http.Request) { result := scanner.ScanSystem() statuses := updater.CheckUpdates(result) type updateInfo struct { Tool string `json:"tool"` Current string `json:"current"` Latest string `json:"latest"` NeedsUpdate bool `json:"needsUpdate"` Error string `json:"error,omitempty"` } updates := make([]updateInfo, len(statuses)) for i, u := range statuses { updates[i] = updateInfo{ Tool: u.Tool, Current: u.Current, Latest: u.Latest, NeedsUpdate: u.NeedsUpdate, Error: u.Error, } } writeJSON(w, map[string]interface{}{ "updates": updates, }) } func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } var body struct { Tools []string `json:"tools"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if len(body.Tools) == 0 { writeError(w, "no tools specified", http.StatusBadRequest) return } writeJSON(w, map[string]string{"status": "installing"}) } func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) { s.scanResult = scanner.ScanSystem() writeJSON(w, map[string]string{"status": "ok"}) } type TermResult struct { Output string `json:"output"` Error string `json:"error,omitempty"` } func (s *Server) handleTerminal(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } var body struct { Command string `json:"command"` Cwd string `json:"cwd"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if body.Command == "" { writeError(w, "no command", http.StatusBadRequest) return } shell := "/bin/sh" if sh := strings.TrimSpace(body.Command); sh != "" { if s, err := exec.LookPath("bash"); err == nil { shell = s } } cmd := exec.Command(shell, "-c", body.Command) if body.Cwd != "" { cmd.Dir = body.Cwd } out, err := cmd.CombinedOutput() result := TermResult{Output: string(out)} if err != nil { result.Error = err.Error() } writeJSON(w, result) }