Major changes: - Refactor CLI entry point to Cobra commands (root, setup, scan, doctor, install, update, lsp, mcp, skills, config, version) - Add LSP registry with health checks, auto-install, and editor config generation - Add MCP registry with editor detection, status tracking, and per-editor configuration - Add workflow engine with planner and step execution for automated task chains - Add conversation search, export (Markdown/JSON), and detailed token counting - Add streaming shell chat handler with tool call/result events - Add skill validation, dry-run testing, and export endpoints - Enrich dashboard with Tools/Activity/Status tabs and tool cards grid - Add PRD documentation - Complete i18n for both EN and FR 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
313 lines
9.5 KiB
Go
313 lines
9.5 KiB
Go
package lsp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type LSPServer struct {
|
|
Name string `json:"name"`
|
|
Language string `json:"language"`
|
|
Command string `json:"command"`
|
|
InstallCmd string `json:"install_cmd"`
|
|
Installed bool `json:"installed"`
|
|
Version string `json:"version,omitempty"`
|
|
Healthy bool `json:"healthy,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Category string `json:"category,omitempty"`
|
|
}
|
|
|
|
var knownServers = []LSPServer{
|
|
{Name: "gopls", Language: "go", Command: "gopls", InstallCmd: "go install golang.org/x/tools/gopls@latest"},
|
|
{Name: "pyright", Language: "python", Command: "pyright", InstallCmd: "npm install -g pyright"},
|
|
{Name: "typescript-language-server", Language: "typescript", Command: "typescript-language-server", InstallCmd: "npm install -g typescript-language-server typescript"},
|
|
{Name: "vscode-json-language-server", Language: "json", Command: "vscode-json-language-server", InstallCmd: "npm install -g vscode-langservers-extracted"},
|
|
{Name: "vscode-html-language-server", Language: "html", Command: "vscode-html-language-server", InstallCmd: "npm install -g vscode-langservers-extracted"},
|
|
{Name: "vscode-css-language-server", Language: "css", Command: "vscode-css-language-server", InstallCmd: "npm install -g vscode-langservers-extracted"},
|
|
{Name: "yaml-language-server", Language: "yaml", Command: "yaml-language-server", InstallCmd: "npm install -g yaml-language-server"},
|
|
{Name: "bash-language-server", Language: "bash", Command: "bash-language-server", InstallCmd: "npm install -g bash-language-server"},
|
|
{Name: "rust-analyzer", Language: "rust", Command: "rust-analyzer", InstallCmd: "rustup component add rust-analyzer"},
|
|
{Name: "clangd", Language: "c/c++", Command: "clangd", InstallCmd: ""},
|
|
{Name: "lua-language-server", Language: "lua", Command: "lua-language-server", InstallCmd: "npm install -g lua-language-server"},
|
|
{Name: "dockerfile-language-server", Language: "dockerfile", Command: "docker-langserver", InstallCmd: "npm install -g dockerfile-language-server-nodejs"},
|
|
{Name: "tailwindcss-language-server", Language: "tailwind", Command: "tailwindcss-language-server", InstallCmd: "npm install -g @tailwindcss/language-server"},
|
|
{Name: "svelte-language-server", Language: "svelte", Command: "svelteserver", InstallCmd: "npm install -g svelte-language-server"},
|
|
{Name: "vue-language-server", Language: "vue", Command: "vue-language-server", InstallCmd: "npm install -g @vue/language-server"},
|
|
{Name: "golangci-lint-langserver", Language: "go-lint", Command: "golangci-lint-langserver", InstallCmd: "go install github.com/nametake/golangci-lint-langserver@latest"},
|
|
}
|
|
|
|
func ScanServers() []LSPServer {
|
|
servers := make([]LSPServer, len(knownServers))
|
|
for i, s := range knownServers {
|
|
servers[i] = s
|
|
_, err := exec.LookPath(s.Command)
|
|
servers[i].Installed = err == nil
|
|
servers[i].Version = getInstalledLSPVersion(s.Name)
|
|
}
|
|
|
|
regServers, err := scanLSPRegistryServers()
|
|
if err == nil {
|
|
servers = append(servers, regServers...)
|
|
}
|
|
|
|
return servers
|
|
}
|
|
|
|
func scanLSPRegistryServers() ([]LSPServer, error) {
|
|
reg, err := LoadLSPRegistry()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
knownNames := map[string]bool{}
|
|
for _, s := range knownServers {
|
|
knownNames[s.Name] = true
|
|
}
|
|
|
|
var servers []LSPServer
|
|
for _, rs := range reg.Servers {
|
|
if knownNames[rs.Name] {
|
|
continue
|
|
}
|
|
servers = append(servers, LSPServer{
|
|
Name: rs.Name,
|
|
Language: rs.Language,
|
|
Command: rs.Command,
|
|
InstallCmd: rs.InstallCmd,
|
|
Installed: isLSPCommandAvailable(rs.Command),
|
|
Description: rs.Description,
|
|
Category: rs.Category,
|
|
Version: getInstalledLSPVersion(rs.Name),
|
|
})
|
|
}
|
|
return servers, nil
|
|
}
|
|
|
|
func isLSPCommandAvailable(cmd string) bool {
|
|
_, err := exec.LookPath(cmd)
|
|
return err == nil
|
|
}
|
|
|
|
func getInstalledLSPVersion(name string) string {
|
|
home, _ := os.UserHomeDir()
|
|
if home == "" {
|
|
return ""
|
|
}
|
|
receiptPath := filepath.Join(home, ".muyue", "receipts", "lsp", name+".json")
|
|
data, err := os.ReadFile(receiptPath)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var receipt struct {
|
|
Version string `json:"version"`
|
|
}
|
|
if json.Unmarshal(data, &receipt) == nil {
|
|
return receipt.Version
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func saveLSPReceipt(name, version string) error {
|
|
home, _ := os.UserHomeDir()
|
|
if home == "" {
|
|
return nil
|
|
}
|
|
receiptDir := filepath.Join(home, ".muyue", "receipts", "lsp")
|
|
os.MkdirAll(receiptDir, 0755)
|
|
|
|
receipt := struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}{
|
|
Name: name,
|
|
Version: version,
|
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
data, _ := json.MarshalIndent(receipt, "", " ")
|
|
return os.WriteFile(filepath.Join(receiptDir, name+".json"), data, 0644)
|
|
}
|
|
|
|
func InstallServer(name string) error {
|
|
for _, s := range knownServers {
|
|
if s.Name == name {
|
|
return doInstallLSP(s)
|
|
}
|
|
}
|
|
|
|
reg, err := LoadLSPRegistry()
|
|
if err == nil {
|
|
for _, s := range reg.Servers {
|
|
if s.Name == name {
|
|
return doInstallLSP(LSPServer{
|
|
Name: s.Name,
|
|
Language: s.Language,
|
|
Command: s.Command,
|
|
InstallCmd: s.InstallCmd,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("unknown LSP server: %s", name)
|
|
}
|
|
|
|
func doInstallLSP(s LSPServer) error {
|
|
if s.InstallCmd == "" {
|
|
return fmt.Errorf("%s has no auto-install command, install manually", s.Name)
|
|
}
|
|
cmd := exec.Command("bash", "-c", s.InstallCmd)
|
|
cmd.Env = os.Environ()
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("install %s: %s: %w", s.Name, string(output), err)
|
|
}
|
|
|
|
saveLSPReceipt(s.Name, "latest")
|
|
return nil
|
|
}
|
|
|
|
func InstallForLanguages(languages []string) []LSPServer {
|
|
langMap := map[string][]string{
|
|
"go": {"gopls"},
|
|
"typescript": {"typescript-language-server", "vscode-json-language-server", "vscode-html-language-server", "vscode-css-language-server"},
|
|
"javascript": {"typescript-language-server", "vscode-json-language-server", "vscode-html-language-server", "vscode-css-language-server"},
|
|
"python": {"pyright"},
|
|
"rust": {"rust-analyzer"},
|
|
"c": {"clangd"},
|
|
"cpp": {"clangd"},
|
|
"json": {"vscode-json-language-server"},
|
|
"yaml": {"yaml-language-server"},
|
|
"bash": {"bash-language-server"},
|
|
"html": {"vscode-html-language-server"},
|
|
"css": {"vscode-css-language-server"},
|
|
"lua": {"lua-language-server"},
|
|
"docker": {"dockerfile-language-server"},
|
|
"svelte": {"svelte-language-server"},
|
|
"vue": {"vue-language-server"},
|
|
}
|
|
|
|
installed := map[string]bool{}
|
|
var results []LSPServer
|
|
|
|
for _, lang := range languages {
|
|
if servers, ok := langMap[lang]; ok {
|
|
for _, srv := range servers {
|
|
if installed[srv] {
|
|
continue
|
|
}
|
|
installed[srv] = true
|
|
if err := InstallServer(srv); err != nil {
|
|
results = append(results, LSPServer{Name: srv, Language: lang, Installed: false})
|
|
} else {
|
|
results = append(results, LSPServer{Name: srv, Language: lang, Installed: true})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func AutoInstallForProject(projectDir string) ([]LSPServer, error) {
|
|
languages := DetectProjectLanguages(projectDir)
|
|
if len(languages) == 0 {
|
|
return nil, nil
|
|
}
|
|
results := InstallForLanguages(languages)
|
|
return results, nil
|
|
}
|
|
|
|
func HealthCheck(name string) (bool, string) {
|
|
for _, s := range knownServers {
|
|
if s.Name == name {
|
|
return healthCheckServer(s)
|
|
}
|
|
}
|
|
return false, "unknown server"
|
|
}
|
|
|
|
func healthCheckServer(s LSPServer) (bool, string) {
|
|
path, err := exec.LookPath(s.Command)
|
|
if err != nil {
|
|
return false, fmt.Sprintf("command %q not found in PATH", s.Command)
|
|
}
|
|
|
|
versionArgs := map[string][]string{
|
|
"gopls": {"version"},
|
|
"pyright": {"--version"},
|
|
"typescript-language-server": {"--version"},
|
|
"rust-analyzer": {"--version"},
|
|
"clangd": {"--version"},
|
|
"lua-language-server": {"--version"},
|
|
"bash-language-server": {"--version"},
|
|
"yaml-language-server": {"--version"},
|
|
}
|
|
|
|
if args, ok := versionArgs[s.Command]; ok {
|
|
cmd := exec.Command(path, args...)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return true, fmt.Sprintf("installed at %s but version check failed", path)
|
|
}
|
|
version := strings.TrimSpace(string(output))
|
|
if idx := strings.Index(version, "\n"); idx > 0 {
|
|
version = version[:idx]
|
|
}
|
|
saveLSPReceipt(s.Name, version)
|
|
return true, version
|
|
}
|
|
|
|
return true, fmt.Sprintf("installed at %s", path)
|
|
}
|
|
|
|
func GenerateEditorConfigs(servers []LSPServer, editor string, homeDir string) (string, error) {
|
|
if homeDir == "" {
|
|
home, _ := os.UserHomeDir()
|
|
homeDir = home
|
|
}
|
|
|
|
reg, err := LoadLSPRegistry()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
regMap := map[string]RegistryEntry{}
|
|
for _, s := range reg.Servers {
|
|
regMap[s.Name] = s
|
|
}
|
|
|
|
var regEntries []RegistryEntry
|
|
for _, s := range servers {
|
|
if re, ok := regMap[s.Name]; ok {
|
|
regEntries = append(regEntries, re)
|
|
}
|
|
}
|
|
|
|
switch editor {
|
|
case "neovim", "nvim":
|
|
return GenerateNeovimConfig(regEntries), nil
|
|
case "helix", "hx":
|
|
return GenerateHelixConfig(regEntries), nil
|
|
case "vscode", "code", "cursor":
|
|
exts := GenerateVSCodeRecommendations(regEntries)
|
|
var b strings.Builder
|
|
b.WriteString("{\n \"recommendations\": [\n")
|
|
for i, ext := range exts {
|
|
if i > 0 {
|
|
b.WriteString(",\n")
|
|
}
|
|
b.WriteString(" \"" + ext + "\"")
|
|
}
|
|
b.WriteString("\n ]\n}")
|
|
return b.String(), nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported editor: %s", editor)
|
|
}
|
|
}
|