Some checks failed
PR Check / check (pull_request) Failing after 11s
New desktop application that launches a local HTTP server with embedded React frontend. Opens in the user's browser automatically. Architecture: - internal/api/: REST API exposing all internal/ packages to frontend - cmd/muyue-desktop/: entry point, serves embedded frontend + API - cmd/muyue-desktop/frontend/: React + Vite SPA Frontend features: - 4 tabs: Dashboard, Studio, Shell, Config - Cyberpunk red theme with CSS custom properties - Theme system: 4 built-in themes (Cyberpunk Red, Pink, Midnight Blue, Matrix Green) - Terminal with command execution via API - Chat interface with sidebar (agents, workflows, commands) - Live clock, status indicators, update badges - Glitch/scanline/fade animations between tabs - xterm.js included for future full terminal integration Backend API endpoints: - GET /api/info, /api/system, /api/tools, /api/config - GET /api/providers, /api/skills, /api/lsp, /api/mcp, /api/updates - POST /api/scan, /api/install, /api/terminal, /api/mcp/configure Build: cd cmd/muyue-desktop/frontend && npm run build && go build ./cmd/muyue-desktop/ Binary: ~11MB single binary with embedded frontend Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
267 lines
6.7 KiB
Go
267 lines
6.7 KiB
Go
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)
|
|
}
|