refactor: unify into single muyue binary with embedded desktop mode
All checks were successful
Beta Release / beta (push) Successful in 37s
All checks were successful
Beta Release / beta (push) Successful in 37s
- 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 <crush@charm.land>
This commit is contained in:
@@ -35,8 +35,8 @@ jobs:
|
|||||||
- name: Cache Node modules
|
- name: Cache Node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: cmd/muyue-desktop/frontend/node_modules
|
path: web/node_modules
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('cmd/muyue-desktop/frontend/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
cd cmd/muyue-desktop/frontend
|
cd web
|
||||||
npm ci
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
echo "beta_num=${BETA_NUM}" >> $GITHUB_OUTPUT
|
echo "beta_num=${BETA_NUM}" >> $GITHUB_OUTPUT
|
||||||
echo "Building beta release: ${VERSION}"
|
echo "Building beta release: ${VERSION}"
|
||||||
|
|
||||||
- name: Build CLI (all platforms)
|
- name: Build (all platforms)
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
VERSION=${{ steps.version.outputs.version }}
|
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=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/
|
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
|
- name: Package archives
|
||||||
run: |
|
run: |
|
||||||
cd dist
|
cd dist
|
||||||
@@ -94,10 +88,9 @@ jobs:
|
|||||||
tar czf muyue-linux-arm64.tar.gz muyue-linux-arm64
|
tar czf muyue-linux-arm64.tar.gz muyue-linux-arm64
|
||||||
tar czf muyue-darwin-amd64.tar.gz muyue-darwin-amd64
|
tar czf muyue-darwin-amd64.tar.gz muyue-darwin-amd64
|
||||||
tar czf muyue-darwin-arm64.tar.gz muyue-darwin-arm64
|
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-amd64.zip muyue-windows-amd64.exe
|
||||||
zip muyue-windows-arm64.zip muyue-windows-arm64.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
|
- name: Generate changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ jobs:
|
|||||||
- name: Cache Node modules
|
- name: Cache Node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: cmd/muyue-desktop/frontend/node_modules
|
path: web/node_modules
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('cmd/muyue-desktop/frontend/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
cd cmd/muyue-desktop/frontend
|
cd web
|
||||||
npm ci
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
echo "base=${BASE_VERSION}" >> $GITHUB_OUTPUT
|
echo "base=${BASE_VERSION}" >> $GITHUB_OUTPUT
|
||||||
echo "Building stable release: ${VERSION}"
|
echo "Building stable release: ${VERSION}"
|
||||||
|
|
||||||
- name: Build CLI (all platforms)
|
- name: Build (all platforms)
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
LDFLAGS="-s -w"
|
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=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/
|
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
|
- name: Package archives
|
||||||
run: |
|
run: |
|
||||||
cd dist
|
cd dist
|
||||||
@@ -93,15 +83,9 @@ jobs:
|
|||||||
tar czf muyue-linux-arm64.tar.gz muyue-linux-arm64
|
tar czf muyue-linux-arm64.tar.gz muyue-linux-arm64
|
||||||
tar czf muyue-darwin-amd64.tar.gz muyue-darwin-amd64
|
tar czf muyue-darwin-amd64.tar.gz muyue-darwin-amd64
|
||||||
tar czf muyue-darwin-arm64.tar.gz muyue-darwin-arm64
|
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-amd64.zip muyue-windows-amd64.exe
|
||||||
zip muyue-windows-arm64.zip muyue-windows-arm64.exe
|
zip muyue-windows-arm64.zip muyue-windows-arm64.exe
|
||||||
zip muyue-desktop-windows-amd64.zip muyue-desktop-windows-amd64.exe
|
rm -f muyue-linux-amd64 muyue-linux-arm64 muyue-darwin-amd64 muyue-darwin-arm64 muyue-windows-amd64.exe muyue-windows-arm64.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
|
|
||||||
|
|
||||||
- name: Generate changelog
|
- name: Generate changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
@@ -135,16 +119,8 @@ jobs:
|
|||||||
echo "| Windows x86_64 | [muyue-windows-amd64.zip](${DL_URL}/muyue-windows-amd64.zip) |"
|
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 "| Windows ARM64 | [muyue-windows-arm64.zip](${DL_URL}/muyue-windows-arm64.zip) |"
|
||||||
echo ""
|
echo ""
|
||||||
echo "### Downloads (Desktop)"
|
echo "The binary includes both CLI and Desktop modes."
|
||||||
echo ""
|
echo "Run \`muyue\` for TUI, \`muyue desktop\` for web UI."
|
||||||
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 ""
|
echo ""
|
||||||
echo "### Install"
|
echo "### Install"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ jobs:
|
|||||||
- name: Cache Node modules
|
- name: Cache Node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: cmd/muyue-desktop/frontend/node_modules
|
path: web/node_modules
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('cmd/muyue-desktop/frontend/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
cd cmd/muyue-desktop/frontend
|
cd web
|
||||||
npm ci
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
@@ -56,5 +56,4 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
go build -o muyue ./cmd/muyue/
|
go build -o muyue ./cmd/muyue/
|
||||||
go build -o muyue-desktop ./cmd/muyue-desktop/
|
|
||||||
./muyue version
|
./muyue version
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -28,4 +28,6 @@ vendor/
|
|||||||
|
|
||||||
# Config with secrets
|
# Config with secrets
|
||||||
.muyue/
|
.muyue/
|
||||||
frontend/node_modules/
|
|
||||||
|
# Frontend (web/.gitignore handles specifics)
|
||||||
|
web/node_modules/
|
||||||
|
|||||||
20
Makefile
20
Makefile
@@ -3,10 +3,16 @@ GOBIN ?= $(GOPATH)/bin
|
|||||||
BINARY = muyue
|
BINARY = muyue
|
||||||
BUILD_DIR = .
|
BUILD_DIR = .
|
||||||
GO = go
|
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/
|
$(GO) build -o $(BUILD_DIR)/$(BINARY) ./cmd/muyue/
|
||||||
|
|
||||||
install: build
|
install: build
|
||||||
@@ -18,6 +24,8 @@ install-local: build
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(BUILD_DIR)/$(BINARY)
|
rm -f $(BUILD_DIR)/$(BINARY)
|
||||||
|
rm -rf $(WEB_DIR)/dist
|
||||||
|
rm -rf $(WEB_DIR)/node_modules
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(GO) test ./... -v -count=1
|
$(GO) test ./... -v -count=1
|
||||||
@@ -31,6 +39,12 @@ vet:
|
|||||||
run: build
|
run: build
|
||||||
./$(BINARY)
|
./$(BINARY)
|
||||||
|
|
||||||
|
desktop: build
|
||||||
|
./$(BINARY) desktop
|
||||||
|
|
||||||
|
dev-desktop:
|
||||||
|
cd $(WEB_DIR) && $(NPM) run dev
|
||||||
|
|
||||||
scan: build
|
scan: build
|
||||||
./$(BINARY) scan
|
./$(BINARY) scan
|
||||||
|
|
||||||
@@ -41,7 +55,7 @@ fmt:
|
|||||||
lint:
|
lint:
|
||||||
which golangci-lint > /dev/null 2>&1 && golangci-lint run || true
|
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=amd64 $(GO) build -o dist/$(BINARY)-linux-amd64 ./cmd/muyue/
|
||||||
GOOS=linux GOARCH=arm64 $(GO) build -o dist/$(BINARY)-linux-arm64 ./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/
|
GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY)-darwin-amd64 ./cmd/muyue/
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }) }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/muyue/muyue/internal/config"
|
"github.com/muyue/muyue/internal/config"
|
||||||
|
"github.com/muyue/muyue/internal/desktop"
|
||||||
"github.com/muyue/muyue/internal/installer"
|
"github.com/muyue/muyue/internal/installer"
|
||||||
"github.com/muyue/muyue/internal/lsp"
|
"github.com/muyue/muyue/internal/lsp"
|
||||||
"github.com/muyue/muyue/internal/mcp"
|
"github.com/muyue/muyue/internal/mcp"
|
||||||
@@ -53,6 +54,8 @@ func handleCommand(args []string) {
|
|||||||
runLSP(args[1:])
|
runLSP(args[1:])
|
||||||
case "mcp":
|
case "mcp":
|
||||||
runMCP(args[1:])
|
runMCP(args[1:])
|
||||||
|
case "desktop":
|
||||||
|
runDesktop(args[1:])
|
||||||
case "skills":
|
case "skills":
|
||||||
runSkills(args[1:])
|
runSkills(args[1:])
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "--help":
|
||||||
@@ -79,6 +82,7 @@ Commands:
|
|||||||
setup Run first-time setup wizard
|
setup Run first-time setup wizard
|
||||||
config Show current configuration
|
config Show current configuration
|
||||||
doctor Check that everything is properly configured
|
doctor Check that everything is properly configured
|
||||||
|
desktop Launch desktop web UI (opens browser)
|
||||||
lsp [scan|install] Scan or install LSP servers
|
lsp [scan|install] Scan or install LSP servers
|
||||||
mcp [config|scan] Configure MCP servers for Crush and Claude Code
|
mcp [config|scan] Configure MCP servers for Crush and Claude Code
|
||||||
skills [list|generate|deploy|init|delete] Manage AI coding skills
|
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 {
|
func loadOrSetupConfig() *config.MuyueConfig {
|
||||||
if !config.Exists() {
|
if !config.Exists() {
|
||||||
fmt.Println("First time setup detected!")
|
fmt.Println("First time setup detected!")
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/muyue/muyue/internal/config"
|
|
||||||
"github.com/muyue/muyue/internal/lsp"
|
"github.com/muyue/muyue/internal/lsp"
|
||||||
"github.com/muyue/muyue/internal/mcp"
|
"github.com/muyue/muyue/internal/mcp"
|
||||||
"github.com/muyue/muyue/internal/scanner"
|
"github.com/muyue/muyue/internal/scanner"
|
||||||
@@ -15,50 +13,6 @@ import (
|
|||||||
"github.com/muyue/muyue/internal/version"
|
"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{}) {
|
func writeJSON(w http.ResponseWriter, data interface{}) {
|
||||||
json.NewEncoder(w).Encode(data)
|
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) {
|
func (s *Server) handleTools(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.scanResult == nil {
|
if s.scanResult == nil {
|
||||||
s.scanResult = scanner.ScanSystem()
|
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 {
|
for i, t := range s.scanResult.Tools {
|
||||||
tools[i] = ToolInfo{
|
tools[i] = toolInfo{
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Installed: t.Installed,
|
Installed: t.Installed,
|
||||||
Version: t.Version,
|
Version: t.Version,
|
||||||
@@ -117,9 +70,9 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]interface{}{
|
writeJSON(w, map[string]interface{}{
|
||||||
"profile": s.config.Profile,
|
"profile": s.config.Profile,
|
||||||
"terminal": s.config.Terminal,
|
"terminal": s.config.Terminal,
|
||||||
"bmad": s.config.BMAD,
|
"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) {
|
func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) {
|
||||||
servers := mcp.ScanServers()
|
servers := mcp.ScanServers()
|
||||||
writeJSON(w, map[string]interface{}{
|
writeJSON(w, map[string]interface{}{
|
||||||
"servers": servers,
|
"servers": servers,
|
||||||
"configured": true,
|
"configured": true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -165,8 +118,7 @@ func (s *Server) handleMCPConfigure(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := mcp.ConfigureAll(s.config)
|
if err := mcp.ConfigureAll(s.config); err != nil {
|
||||||
if err != nil {
|
|
||||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -222,11 +174,6 @@ func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, map[string]string{"status": "ok"})
|
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) {
|
func (s *Server) handleTerminal(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
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)
|
writeError(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if body.Command == "" {
|
if body.Command == "" {
|
||||||
writeError(w, "no command", http.StatusBadRequest)
|
writeError(w, "no command", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
shell := "/bin/sh"
|
shell := "/bin/sh"
|
||||||
if sh := strings.TrimSpace(body.Command); sh != "" {
|
if s, err := exec.LookPath("bash"); err == nil {
|
||||||
if s, err := exec.LookPath("bash"); err == nil {
|
shell = s
|
||||||
shell = s
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(shell, "-c", body.Command)
|
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
|
cmd.Dir = body.Cwd
|
||||||
}
|
}
|
||||||
out, err := cmd.CombinedOutput()
|
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 {
|
if err != nil {
|
||||||
result.Error = err.Error()
|
result.Error = err.Error()
|
||||||
}
|
}
|
||||||
52
internal/api/server.go
Normal file
52
internal/api/server.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
131
internal/desktop/desktop.go
Normal file
131
internal/desktop/desktop.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
!dist/.gitkeep
|
||||||
.vite/
|
.vite/
|
||||||
6
web/embed.go
Normal file
6
web/embed.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed all:dist
|
||||||
|
var Assets embed.FS
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "muyue-web",
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "muyue-web",
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
|
||||||
"@xterm/xterm": "^6.0.0",
|
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5"
|
||||||
"xterm": "^5.3.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@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": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -981,13 +960,6 @@
|
|||||||
"optional": true
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
18
web/package.json
Normal file
18
web/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
web/src/api/client.js
Normal file
31
web/src/api/client.js
Normal file
@@ -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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { useAPI } from '../hooks/useAPI'
|
import api from '../api/client'
|
||||||
import { getTheme, getThemeNames, applyTheme } from '../themes'
|
import { getTheme, getThemeNames, applyTheme } from '../themes'
|
||||||
import Dashboard from './Dashboard'
|
import Dashboard from './Dashboard'
|
||||||
import Studio from './Studio'
|
import Studio from './Studio'
|
||||||
@@ -21,7 +21,7 @@ export default function App() {
|
|||||||
const [tools, setTools] = useState([])
|
const [tools, setTools] = useState([])
|
||||||
const [transition, setTransition] = useState(false)
|
const [transition, setTransition] = useState(false)
|
||||||
const [currentTheme, setCurrentTheme] = useState('cyberpunk-red')
|
const [currentTheme, setCurrentTheme] = useState('cyberpunk-red')
|
||||||
const api = useAPI()
|
// api is imported directly
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getInfo().then(setInfo).catch(() => {})
|
api.getInfo().then(setInfo).catch(() => {})
|
||||||
@@ -8,8 +8,12 @@ export default defineConfig({
|
|||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://127.0.0.1:0',
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:8095',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user