From 34636056daa5d6fbbd531b5b5572401b4e031f65 Mon Sep 17 00:00:00 2001 From: Augustin Date: Tue, 21 Apr 2026 21:04:47 +0200 Subject: [PATCH] refactor: unify into single `muyue` binary with embedded desktop mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge muyue + muyue-desktop into one binary (13MB) - `muyue` starts TUI, `muyue desktop` launches web UI in browser - Move frontend from cmd/muyue-desktop/frontend/ to web/ (standard Go layout) - Add web/embed.go with //go:embed all:dist for frontend assets - Add internal/desktop/ package (server, browser open, SPA routing, signals) - Split internal/api/api.go into server.go + handlers.go - Add internal/desktop/desktop.go with SPA fallback and --port/--no-open flags - Clean package.json: remove unused @xterm/xterm, switch to ESM - Fix vite.config.js proxy to use port 8095 for dev mode - Add Makefile targets: frontend, desktop, dev-desktop - Update all CI workflows: single binary build, web/ paths - Remove cmd/muyue-desktop/ entirely 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush --- .gitea/workflows/ci-develop.yml | 17 +-- .gitea/workflows/ci-main.yml | 38 +---- .gitea/workflows/ci-pr.yml | 7 +- .gitignore | 4 +- Makefile | 20 ++- cmd/muyue-desktop/frontend/package.json | 26 ---- .../frontend/src/hooks/useAPI.js | 31 ----- cmd/muyue-desktop/main.go | 116 ---------------- cmd/muyue/main.go | 12 ++ internal/api/{api.go => handlers.go} | 93 +++---------- internal/api/server.go | 52 +++++++ internal/desktop/desktop.go | 131 ++++++++++++++++++ .../muyue-desktop/frontend => web}/.gitignore | 1 + web/embed.go | 6 + .../muyue-desktop/frontend => web}/index.html | 0 .../frontend => web}/package-lock.json | 34 +---- web/package.json | 18 +++ web/src/api/client.js | 31 +++++ .../frontend => web}/src/components/App.jsx | 4 +- .../src/components/Config.jsx | 0 .../src/components/Dashboard.jsx | 0 .../frontend => web}/src/components/Shell.jsx | 0 .../src/components/Studio.jsx | 0 .../frontend => web}/src/main.jsx | 0 .../frontend => web}/src/styles/global.css | 0 .../frontend => web}/src/themes/index.js | 0 .../frontend => web}/vite.config.js | 6 +- 27 files changed, 317 insertions(+), 330 deletions(-) delete mode 100644 cmd/muyue-desktop/frontend/package.json delete mode 100644 cmd/muyue-desktop/frontend/src/hooks/useAPI.js delete mode 100644 cmd/muyue-desktop/main.go rename internal/api/{api.go => handlers.go} (69%) create mode 100644 internal/api/server.go create mode 100644 internal/desktop/desktop.go rename {cmd/muyue-desktop/frontend => web}/.gitignore (64%) create mode 100644 web/embed.go rename {cmd/muyue-desktop/frontend => web}/index.html (100%) rename {cmd/muyue-desktop/frontend => web}/package-lock.json (96%) create mode 100644 web/package.json create mode 100644 web/src/api/client.js rename {cmd/muyue-desktop/frontend => web}/src/components/App.jsx (98%) rename {cmd/muyue-desktop/frontend => web}/src/components/Config.jsx (100%) rename {cmd/muyue-desktop/frontend => web}/src/components/Dashboard.jsx (100%) rename {cmd/muyue-desktop/frontend => web}/src/components/Shell.jsx (100%) rename {cmd/muyue-desktop/frontend => web}/src/components/Studio.jsx (100%) rename {cmd/muyue-desktop/frontend => web}/src/main.jsx (100%) rename {cmd/muyue-desktop/frontend => web}/src/styles/global.css (100%) rename {cmd/muyue-desktop/frontend => web}/src/themes/index.js (100%) rename {cmd/muyue-desktop/frontend => web}/vite.config.js (67%) diff --git a/.gitea/workflows/ci-develop.yml b/.gitea/workflows/ci-develop.yml index 4e8ee2d..ab354f7 100644 --- a/.gitea/workflows/ci-develop.yml +++ b/.gitea/workflows/ci-develop.yml @@ -35,8 +35,8 @@ jobs: - name: Cache Node modules uses: actions/cache@v4 with: - path: cmd/muyue-desktop/frontend/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('cmd/muyue-desktop/frontend/package-lock.json') }} + path: web/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- @@ -45,7 +45,7 @@ jobs: - name: Build frontend run: | - cd cmd/muyue-desktop/frontend + cd web npm ci npm run build @@ -68,7 +68,7 @@ jobs: echo "beta_num=${BETA_NUM}" >> $GITHUB_OUTPUT echo "Building beta release: ${VERSION}" - - name: Build CLI (all platforms) + - name: Build (all platforms) run: | mkdir -p dist VERSION=${{ steps.version.outputs.version }} @@ -80,12 +80,6 @@ jobs: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-windows-amd64.exe ./cmd/muyue/ CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-windows-arm64.exe ./cmd/muyue/ - - name: Build Desktop (linux amd64) - run: | - VERSION=${{ steps.version.outputs.version }} - LDFLAGS="-s -w -X github.com/muyue/muyue/internal/version.Prerelease=${VERSION#v}" - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-desktop-linux-amd64 ./cmd/muyue-desktop/ - - name: Package archives run: | cd dist @@ -94,10 +88,9 @@ jobs: tar czf muyue-linux-arm64.tar.gz muyue-linux-arm64 tar czf muyue-darwin-amd64.tar.gz muyue-darwin-amd64 tar czf muyue-darwin-arm64.tar.gz muyue-darwin-arm64 - tar czf muyue-desktop-linux-amd64.tar.gz muyue-desktop-linux-amd64 zip muyue-windows-amd64.zip muyue-windows-amd64.exe zip muyue-windows-arm64.zip muyue-windows-arm64.exe - rm -f muyue-linux-amd64 muyue-linux-arm64 muyue-darwin-amd64 muyue-darwin-arm64 muyue-windows-amd64.exe muyue-windows-arm64.exe muyue-desktop-linux-amd64 + rm -f muyue-linux-amd64 muyue-linux-arm64 muyue-darwin-amd64 muyue-darwin-arm64 muyue-windows-amd64.exe muyue-windows-arm64.exe - name: Generate changelog id: changelog diff --git a/.gitea/workflows/ci-main.yml b/.gitea/workflows/ci-main.yml index 75f0b60..df6926c 100644 --- a/.gitea/workflows/ci-main.yml +++ b/.gitea/workflows/ci-main.yml @@ -35,8 +35,8 @@ jobs: - name: Cache Node modules uses: actions/cache@v4 with: - path: cmd/muyue-desktop/frontend/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('cmd/muyue-desktop/frontend/package-lock.json') }} + path: web/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- @@ -45,7 +45,7 @@ jobs: - name: Build frontend run: | - cd cmd/muyue-desktop/frontend + cd web npm ci npm run build @@ -64,7 +64,7 @@ jobs: echo "base=${BASE_VERSION}" >> $GITHUB_OUTPUT echo "Building stable release: ${VERSION}" - - name: Build CLI (all platforms) + - name: Build (all platforms) run: | mkdir -p dist LDFLAGS="-s -w" @@ -75,16 +75,6 @@ jobs: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-windows-amd64.exe ./cmd/muyue/ CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-windows-arm64.exe ./cmd/muyue/ - - name: Build Desktop (all platforms) - run: | - LDFLAGS="-s -w" - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-desktop-linux-amd64 ./cmd/muyue-desktop/ - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-desktop-linux-arm64 ./cmd/muyue-desktop/ - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-desktop-darwin-amd64 ./cmd/muyue-desktop/ - CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-desktop-darwin-arm64 ./cmd/muyue-desktop/ - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/muyue-desktop-windows-amd64.exe ./cmd/muyue-desktop/ - CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/muyue-desktop-windows-arm64.exe ./cmd/muyue-desktop/ - - name: Package archives run: | cd dist @@ -93,15 +83,9 @@ jobs: tar czf muyue-linux-arm64.tar.gz muyue-linux-arm64 tar czf muyue-darwin-amd64.tar.gz muyue-darwin-amd64 tar czf muyue-darwin-arm64.tar.gz muyue-darwin-arm64 - tar czf muyue-desktop-linux-amd64.tar.gz muyue-desktop-linux-amd64 - tar czf muyue-desktop-linux-arm64.tar.gz muyue-desktop-linux-arm64 - tar czf muyue-desktop-darwin-amd64.tar.gz muyue-desktop-darwin-amd64 - tar czf muyue-desktop-darwin-arm64.tar.gz muyue-desktop-darwin-arm64 zip muyue-windows-amd64.zip muyue-windows-amd64.exe zip muyue-windows-arm64.zip muyue-windows-arm64.exe - zip muyue-desktop-windows-amd64.zip muyue-desktop-windows-amd64.exe - zip muyue-desktop-windows-arm64.zip muyue-desktop-windows-arm64.exe - rm -f muyue-linux-amd64 muyue-linux-arm64 muyue-darwin-amd64 muyue-darwin-arm64 muyue-windows-amd64.exe muyue-windows-arm64.exe muyue-desktop-linux-amd64 muyue-desktop-linux-arm64 muyue-desktop-darwin-amd64 muyue-desktop-darwin-arm64 muyue-desktop-windows-amd64.exe muyue-desktop-windows-arm64.exe + rm -f muyue-linux-amd64 muyue-linux-arm64 muyue-darwin-amd64 muyue-darwin-arm64 muyue-windows-amd64.exe muyue-windows-arm64.exe - name: Generate changelog id: changelog @@ -135,16 +119,8 @@ jobs: echo "| Windows x86_64 | [muyue-windows-amd64.zip](${DL_URL}/muyue-windows-amd64.zip) |" echo "| Windows ARM64 | [muyue-windows-arm64.zip](${DL_URL}/muyue-windows-arm64.zip) |" echo "" - echo "### Downloads (Desktop)" - echo "" - echo "| Platform | File |" - echo "|----------|------|" - echo "| Linux x86_64 | [muyue-desktop-linux-amd64.tar.gz](${DL_URL}/muyue-desktop-linux-amd64.tar.gz) |" - echo "| Linux ARM64 | [muyue-desktop-linux-arm64.tar.gz](${DL_URL}/muyue-desktop-linux-arm64.tar.gz) |" - echo "| macOS Intel | [muyue-desktop-darwin-amd64.tar.gz](${DL_URL}/muyue-desktop-darwin-amd64.tar.gz) |" - echo "| macOS Apple Silicon | [muyue-desktop-darwin-arm64.tar.gz](${DL_URL}/muyue-desktop-darwin-arm64.tar.gz) |" - echo "| Windows x86_64 | [muyue-desktop-windows-amd64.zip](${DL_URL}/muyue-desktop-windows-amd64.zip) |" - echo "| Windows ARM64 | [muyue-desktop-windows-arm64.zip](${DL_URL}/muyue-desktop-windows-arm64.zip) |" + echo "The binary includes both CLI and Desktop modes." + echo "Run \`muyue\` for TUI, \`muyue desktop\` for web UI." echo "" echo "### Install" echo "" diff --git a/.gitea/workflows/ci-pr.yml b/.gitea/workflows/ci-pr.yml index b9ac645..0e1c784 100644 --- a/.gitea/workflows/ci-pr.yml +++ b/.gitea/workflows/ci-pr.yml @@ -33,8 +33,8 @@ jobs: - name: Cache Node modules uses: actions/cache@v4 with: - path: cmd/muyue-desktop/frontend/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('cmd/muyue-desktop/frontend/package-lock.json') }} + path: web/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- @@ -43,7 +43,7 @@ jobs: - name: Build frontend run: | - cd cmd/muyue-desktop/frontend + cd web npm ci npm run build @@ -56,5 +56,4 @@ jobs: - name: Build run: | go build -o muyue ./cmd/muyue/ - go build -o muyue-desktop ./cmd/muyue-desktop/ ./muyue version diff --git a/.gitignore b/.gitignore index 60ffe05..5b64cac 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ vendor/ # Config with secrets .muyue/ -frontend/node_modules/ + +# Frontend (web/.gitignore handles specifics) +web/node_modules/ diff --git a/Makefile b/Makefile index 181d9b7..19edb87 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,16 @@ GOBIN ?= $(GOPATH)/bin BINARY = muyue BUILD_DIR = . GO = go +NODE ?= node +NPM ?= npm +WEB_DIR = web -.PHONY: build install clean test test-short run scan fmt lint build-all deps vet +.PHONY: build install clean test test-short run scan fmt lint build-all deps vet frontend dev-desktop -build: +frontend: + cd $(WEB_DIR) && $(NPM) ci && $(NPM) run build + +build: frontend $(GO) build -o $(BUILD_DIR)/$(BINARY) ./cmd/muyue/ install: build @@ -18,6 +24,8 @@ install-local: build clean: rm -f $(BUILD_DIR)/$(BINARY) + rm -rf $(WEB_DIR)/dist + rm -rf $(WEB_DIR)/node_modules test: $(GO) test ./... -v -count=1 @@ -31,6 +39,12 @@ vet: run: build ./$(BINARY) +desktop: build + ./$(BINARY) desktop + +dev-desktop: + cd $(WEB_DIR) && $(NPM) run dev + scan: build ./$(BINARY) scan @@ -41,7 +55,7 @@ fmt: lint: which golangci-lint > /dev/null 2>&1 && golangci-lint run || true -build-all: +build-all: frontend GOOS=linux GOARCH=amd64 $(GO) build -o dist/$(BINARY)-linux-amd64 ./cmd/muyue/ GOOS=linux GOARCH=arm64 $(GO) build -o dist/$(BINARY)-linux-arm64 ./cmd/muyue/ GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY)-darwin-amd64 ./cmd/muyue/ diff --git a/cmd/muyue-desktop/frontend/package.json b/cmd/muyue-desktop/frontend/package.json deleted file mode 100644 index c41363c..0000000 --- a/cmd/muyue-desktop/frontend/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "frontend", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "keywords": [], - "author": "", - "license": "ISC", - "type": "commonjs", - "dependencies": { - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "react": "^19.2.5", - "react-dom": "^19.2.5", - "xterm": "^5.3.0" - }, - "devDependencies": { - "@vitejs/plugin-react": "^6.0.1", - "vite": "^8.0.9" - } -} diff --git a/cmd/muyue-desktop/frontend/src/hooks/useAPI.js b/cmd/muyue-desktop/frontend/src/hooks/useAPI.js deleted file mode 100644 index d8fc8af..0000000 --- a/cmd/muyue-desktop/frontend/src/hooks/useAPI.js +++ /dev/null @@ -1,31 +0,0 @@ -const API_BASE = '/api' - -async function request(path, options = {}) { - const res = await fetch(`${API_BASE}${path}`, { - headers: { 'Content-Type': 'application/json' }, - ...options, - }) - if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })) - throw new Error(err.error || res.statusText) - } - return res.json() -} - -export function useAPI() { - return { - getInfo: () => request('/info'), - getSystem: () => request('/system'), - getTools: () => request('/tools'), - getConfig: () => request('/config'), - getProviders: () => request('/providers'), - getSkills: () => request('/skills'), - getLSP: () => request('/lsp'), - getMCP: () => request('/mcp'), - getUpdates: () => request('/updates'), - runScan: () => request('/scan', { method: 'POST' }), - installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }), - configureMCP: () => request('/mcp/configure', { method: 'POST' }), - runCommand: (command, cwd) => request('/terminal', { method: 'POST', body: JSON.stringify({ command, cwd }) }), - } -} diff --git a/cmd/muyue-desktop/main.go b/cmd/muyue-desktop/main.go deleted file mode 100644 index 24bdffc..0000000 --- a/cmd/muyue-desktop/main.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "embed" - "fmt" - "io/fs" - "log" - "net" - "net/http" - "os" - "os/exec" - "os/signal" - "syscall" - - "github.com/muyue/muyue/internal/api" - "github.com/muyue/muyue/internal/config" - "github.com/muyue/muyue/internal/version" -) - -//go:embed frontend/dist/* -var frontendFS embed.FS - -func main() { - if len(os.Args) > 1 && os.Args[1] == "--help" { - fmt.Printf("%s Desktop v%s\n", version.Name, version.Version) - fmt.Println("Usage: muyue-desktop [options]") - fmt.Println() - fmt.Println("Options:") - fmt.Println(" --help Show this help") - fmt.Println(" --port Specify port (default: auto)") - fmt.Println(" --no-open Don't open browser") - return - } - - cfg := loadConfig() - srv := api.NewServer(cfg) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - log.Fatalf("Failed to bind: %v", err) - } - addr := listener.Addr().(*net.TCPAddr) - port := addr.Port - - frontendDist, err := fs.Sub(frontendFS, "frontend/dist") - if err != nil { - log.Fatalf("Failed to load frontend: %v", err) - } - - fileServer := http.FileServer(http.FS(frontendDist)) - - mux := http.NewServeMux() - mux.Handle("/api/", srv) - mux.Handle("/", fileServer) - - go func() { - log.Printf("%s Desktop v%s", version.Name, version.Version) - log.Printf("Listening on http://127.0.0.1:%d", port) - - if err := http.Serve(listener, mux); err != nil { - log.Fatalf("Server error: %v", err) - } - }() - - noOpen := false - for _, arg := range os.Args[1:] { - if arg == "--no-open" { - noOpen = true - } - } - - if !noOpen { - url := fmt.Sprintf("http://127.0.0.1:%d", port) - openBrowser(url) - log.Printf("Opened %s in browser", url) - } - - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("Shutting down...") -} - -func loadConfig() *config.MuyueConfig { - if !config.Exists() { - fmt.Println("No config found. Run `muyue setup` first.") - os.Exit(1) - } - cfg, err := config.Load() - if err != nil { - fmt.Fprintf(os.Stderr, "Config error: %v\n", err) - os.Exit(1) - } - return cfg -} - -func openBrowser(url string) { - var cmd *exec.Cmd - switch { - case commandExists("xdg-open"): - cmd = exec.Command("xdg-open", url) - case commandExists("open"): - cmd = exec.Command("open", url) - case commandExists("cmd"): - cmd = exec.Command("cmd", "/c", "start", url) - default: - fmt.Printf("Open manually: %s\n", url) - return - } - cmd.Start() -} - -func commandExists(name string) bool { - _, err := exec.LookPath(name) - return err == nil -} diff --git a/cmd/muyue/main.go b/cmd/muyue/main.go index 820553d..3da658d 100644 --- a/cmd/muyue/main.go +++ b/cmd/muyue/main.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/muyue/muyue/internal/config" + "github.com/muyue/muyue/internal/desktop" "github.com/muyue/muyue/internal/installer" "github.com/muyue/muyue/internal/lsp" "github.com/muyue/muyue/internal/mcp" @@ -53,6 +54,8 @@ func handleCommand(args []string) { runLSP(args[1:]) case "mcp": runMCP(args[1:]) + case "desktop": + runDesktop(args[1:]) case "skills": runSkills(args[1:]) case "help", "-h", "--help": @@ -79,6 +82,7 @@ Commands: setup Run first-time setup wizard config Show current configuration doctor Check that everything is properly configured + desktop Launch desktop web UI (opens browser) lsp [scan|install] Scan or install LSP servers mcp [config|scan] Configure MCP servers for Crush and Claude Code skills [list|generate|deploy|init|delete] Manage AI coding skills @@ -118,6 +122,14 @@ func runTUI() { } } +func runDesktop(args []string) { + cfg := loadOrSetupConfig() + if err := desktop.Run(cfg, args); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + func loadOrSetupConfig() *config.MuyueConfig { if !config.Exists() { fmt.Println("First time setup detected!") diff --git a/internal/api/api.go b/internal/api/handlers.go similarity index 69% rename from internal/api/api.go rename to internal/api/handlers.go index d8a6719..ed2e79b 100644 --- a/internal/api/api.go +++ b/internal/api/handlers.go @@ -4,9 +4,7 @@ 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" @@ -15,50 +13,6 @@ import ( "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) } @@ -85,20 +39,19 @@ func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) { }) } -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)) + type toolInfo struct { + Name string `json:"name"` + Installed bool `json:"installed"` + Version string `json:"version"` + Path string `json:"path"` + } + tools := make([]toolInfo, len(s.scanResult.Tools)) for i, t := range s.scanResult.Tools { - tools[i] = ToolInfo{ + tools[i] = toolInfo{ Name: t.Name, Installed: t.Installed, Version: t.Version, @@ -117,9 +70,9 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { return } writeJSON(w, map[string]interface{}{ - "profile": s.config.Profile, - "terminal": s.config.Terminal, - "bmad": s.config.BMAD, + "profile": s.config.Profile, + "terminal": s.config.Terminal, + "bmad": s.config.BMAD, }) } @@ -155,7 +108,7 @@ func (s *Server) handleLSP(w http.ResponseWriter, r *http.Request) { func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) { servers := mcp.ScanServers() writeJSON(w, map[string]interface{}{ - "servers": servers, + "servers": servers, "configured": true, }) } @@ -165,8 +118,7 @@ func (s *Server) handleMCPConfigure(w http.ResponseWriter, r *http.Request) { writeError(w, "POST only", http.StatusMethodNotAllowed) return } - err := mcp.ConfigureAll(s.config) - if err != nil { + if err := mcp.ConfigureAll(s.config); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } @@ -222,11 +174,6 @@ func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) { 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) @@ -240,17 +187,14 @@ func (s *Server) handleTerminal(w http.ResponseWriter, r *http.Request) { 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 - } + if s, err := exec.LookPath("bash"); err == nil { + shell = s } cmd := exec.Command(shell, "-c", body.Command) @@ -258,7 +202,12 @@ func (s *Server) handleTerminal(w http.ResponseWriter, r *http.Request) { cmd.Dir = body.Cwd } out, err := cmd.CombinedOutput() - result := TermResult{Output: string(out)} + + type termResult struct { + Output string `json:"output"` + Error string `json:"error,omitempty"` + } + result := termResult{Output: string(out)} if err != nil { result.Error = err.Error() } diff --git a/internal/api/server.go b/internal/api/server.go new file mode 100644 index 0000000..a171d7c --- /dev/null +++ b/internal/api/server.go @@ -0,0 +1,52 @@ +package api + +import ( + "net/http" + + "github.com/muyue/muyue/internal/config" + "github.com/muyue/muyue/internal/scanner" +) + +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) +} diff --git a/internal/desktop/desktop.go b/internal/desktop/desktop.go new file mode 100644 index 0000000..23d2241 --- /dev/null +++ b/internal/desktop/desktop.go @@ -0,0 +1,131 @@ +package desktop + +import ( + "fmt" + "io/fs" + "log" + "net" + "net/http" + "os" + "os/exec" + "os/signal" + "strconv" + "strings" + "syscall" + + "github.com/muyue/muyue/internal/api" + "github.com/muyue/muyue/internal/config" + "github.com/muyue/muyue/internal/version" + "github.com/muyue/muyue/web" +) + +type options struct { + port int + noOpen bool +} + +type option func(*options) + +func WithPort(port int) option { + return func(o *options) { o.port = port } +} + +func WithNoOpen(noOpen bool) option { + return func(o *options) { o.noOpen = noOpen } +} + +func parseFlags(args []string) []option { + var opts []option + for _, arg := range args { + switch { + case arg == "--no-open": + opts = append(opts, WithNoOpen(true)) + case strings.HasPrefix(arg, "--port="): + if p, err := strconv.Atoi(strings.TrimPrefix(arg, "--port=")); err == nil { + opts = append(opts, WithPort(p)) + } + case arg == "--port": + // handled as prefix case + } + } + return opts +} + +func Run(cfg *config.MuyueConfig, args []string) error { + o := options{} + for _, opt := range parseFlags(args) { + opt(&o) + } + + log.Printf("%s Desktop v%s", version.Name, version.Version) + + srv := api.NewServer(cfg) + + frontendFS, err := fs.Sub(web.Assets, "dist") + if err != nil { + return fmt.Errorf("frontend assets: %w", err) + } + + mux := http.NewServeMux() + mux.Handle("/api/", srv) + mux.Handle("/", spaHandler(http.FileServer(http.FS(frontendFS)))) + + addr := fmt.Sprintf("127.0.0.1:%d", o.port) + listener, err := net.Listen("tcp", addr) + if err != nil { + return fmt.Errorf("bind %s: %w", addr, err) + } + port := listener.Addr().(*net.TCPAddr).Port + + go func() { + if err := http.Serve(listener, mux); err != nil { + log.Fatalf("Server error: %v", err) + } + }() + + url := fmt.Sprintf("http://127.0.0.1:%d", port) + log.Printf("Listening on %s", url) + + if !o.noOpen { + openBrowser(url) + log.Printf("Opened browser") + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down...") + return nil +} + +func spaHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if path != "/" && !strings.Contains(path, ".") { + r.URL.Path = "/" + } + next.ServeHTTP(w, r) + }) +} + +func openBrowser(url string) { + var cmd *exec.Cmd + switch { + case exists("xdg-open"): + cmd = exec.Command("xdg-open", url) + case exists("open"): + cmd = exec.Command("open", url) + case exists("cmd"): + cmd = exec.Command("cmd", "/c", "start", url) + default: + fmt.Printf("Open manually: %s\n", url) + return + } + _ = cmd.Start() +} + +func exists(name string) bool { + _, err := exec.LookPath(name) + return err == nil +} diff --git a/cmd/muyue-desktop/frontend/.gitignore b/web/.gitignore similarity index 64% rename from cmd/muyue-desktop/frontend/.gitignore rename to web/.gitignore index 3ff38cc..7d69ebd 100644 --- a/cmd/muyue-desktop/frontend/.gitignore +++ b/web/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ +!dist/.gitkeep .vite/ diff --git a/web/embed.go b/web/embed.go new file mode 100644 index 0000000..bff6d13 --- /dev/null +++ b/web/embed.go @@ -0,0 +1,6 @@ +package web + +import "embed" + +//go:embed all:dist +var Assets embed.FS diff --git a/cmd/muyue-desktop/frontend/index.html b/web/index.html similarity index 100% rename from cmd/muyue-desktop/frontend/index.html rename to web/index.html diff --git a/cmd/muyue-desktop/frontend/package-lock.json b/web/package-lock.json similarity index 96% rename from cmd/muyue-desktop/frontend/package-lock.json rename to web/package-lock.json index de8e8ab..a128dc7 100644 --- a/cmd/muyue-desktop/frontend/package-lock.json +++ b/web/package-lock.json @@ -1,19 +1,13 @@ { - "name": "frontend", - "version": "1.0.0", + "name": "muyue-web", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frontend", - "version": "1.0.0", - "license": "ISC", + "name": "muyue-web", "dependencies": { - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", "react": "^19.2.5", - "react-dom": "^19.2.5", - "xterm": "^5.3.0" + "react-dom": "^19.2.5" }, "devDependencies": { "@vitejs/plugin-react": "^6.0.1", @@ -402,21 +396,6 @@ } } }, - "node_modules/@xterm/addon-fit": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", - "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", - "license": "MIT" - }, - "node_modules/@xterm/xterm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", - "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", - "license": "MIT", - "workspaces": [ - "addons/*" - ] - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -981,13 +960,6 @@ "optional": true } } - }, - "node_modules/xterm": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", - "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", - "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", - "license": "MIT" } } } diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..6f36f7a --- /dev/null +++ b/web/package.json @@ -0,0 +1,18 @@ +{ + "name": "muyue-web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.5", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.9" + } +} diff --git a/web/src/api/client.js b/web/src/api/client.js new file mode 100644 index 0000000..cbeb187 --- /dev/null +++ b/web/src/api/client.js @@ -0,0 +1,31 @@ +const API_BASE = '/api' + +async function request(path, options = {}) { + const res = await fetch(`${API_BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...options, + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })) + throw new Error(err.error || res.statusText) + } + return res.json() +} + +const api = { + getInfo: () => request('/info'), + getSystem: () => request('/system'), + getTools: () => request('/tools'), + getConfig: () => request('/config'), + getProviders: () => request('/providers'), + getSkills: () => request('/skills'), + getLSP: () => request('/lsp'), + getMCP: () => request('/mcp'), + getUpdates: () => request('/updates'), + runScan: () => request('/scan', { method: 'POST' }), + installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }), + configureMCP: () => request('/mcp/configure', { method: 'POST' }), + runCommand: (command, cwd) => request('/terminal', { method: 'POST', body: JSON.stringify({ command, cwd }) }), +} + +export default api diff --git a/cmd/muyue-desktop/frontend/src/components/App.jsx b/web/src/components/App.jsx similarity index 98% rename from cmd/muyue-desktop/frontend/src/components/App.jsx rename to web/src/components/App.jsx index 3e1a633..896469d 100644 --- a/cmd/muyue-desktop/frontend/src/components/App.jsx +++ b/web/src/components/App.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { useAPI } from '../hooks/useAPI' +import api from '../api/client' import { getTheme, getThemeNames, applyTheme } from '../themes' import Dashboard from './Dashboard' import Studio from './Studio' @@ -21,7 +21,7 @@ export default function App() { const [tools, setTools] = useState([]) const [transition, setTransition] = useState(false) const [currentTheme, setCurrentTheme] = useState('cyberpunk-red') - const api = useAPI() + // api is imported directly useEffect(() => { api.getInfo().then(setInfo).catch(() => {}) diff --git a/cmd/muyue-desktop/frontend/src/components/Config.jsx b/web/src/components/Config.jsx similarity index 100% rename from cmd/muyue-desktop/frontend/src/components/Config.jsx rename to web/src/components/Config.jsx diff --git a/cmd/muyue-desktop/frontend/src/components/Dashboard.jsx b/web/src/components/Dashboard.jsx similarity index 100% rename from cmd/muyue-desktop/frontend/src/components/Dashboard.jsx rename to web/src/components/Dashboard.jsx diff --git a/cmd/muyue-desktop/frontend/src/components/Shell.jsx b/web/src/components/Shell.jsx similarity index 100% rename from cmd/muyue-desktop/frontend/src/components/Shell.jsx rename to web/src/components/Shell.jsx diff --git a/cmd/muyue-desktop/frontend/src/components/Studio.jsx b/web/src/components/Studio.jsx similarity index 100% rename from cmd/muyue-desktop/frontend/src/components/Studio.jsx rename to web/src/components/Studio.jsx diff --git a/cmd/muyue-desktop/frontend/src/main.jsx b/web/src/main.jsx similarity index 100% rename from cmd/muyue-desktop/frontend/src/main.jsx rename to web/src/main.jsx diff --git a/cmd/muyue-desktop/frontend/src/styles/global.css b/web/src/styles/global.css similarity index 100% rename from cmd/muyue-desktop/frontend/src/styles/global.css rename to web/src/styles/global.css diff --git a/cmd/muyue-desktop/frontend/src/themes/index.js b/web/src/themes/index.js similarity index 100% rename from cmd/muyue-desktop/frontend/src/themes/index.js rename to web/src/themes/index.js diff --git a/cmd/muyue-desktop/frontend/vite.config.js b/web/vite.config.js similarity index 67% rename from cmd/muyue-desktop/frontend/vite.config.js rename to web/vite.config.js index ad12586..e3523f9 100644 --- a/cmd/muyue-desktop/frontend/vite.config.js +++ b/web/vite.config.js @@ -8,8 +8,12 @@ export default defineConfig({ emptyOutDir: true, }, server: { + port: 5173, proxy: { - '/api': 'http://127.0.0.1:0', + '/api': { + target: 'http://127.0.0.1:8095', + changeOrigin: true, + }, }, }, })