feat: initial commit
This commit is contained in:
commit
8a467d9004
5 changed files with 828 additions and 0 deletions
32
go.mod
Normal file
32
go.mod
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
module lsp-manager
|
||||||
|
|
||||||
|
go 1.25.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/adrg/xdg v0.5.3
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
65
go.sum
Normal file
65
go.sum
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||||
|
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
196
install.go
Normal file
196
install.go
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
// "github.com/adrg/xdg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Installer verwaltet die Installation von LSP-Servern
|
||||||
|
type Installer struct {
|
||||||
|
installDir string
|
||||||
|
binDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstaller erstellt einen XDG-konformen Installer
|
||||||
|
func NewInstaller(baseDir string) *Installer {
|
||||||
|
installDir := filepath.Join(baseDir, "servers")
|
||||||
|
binDir := filepath.Join(baseDir, "bin")
|
||||||
|
|
||||||
|
// Verzeichnisse erstellen
|
||||||
|
os.MkdirAll(installDir, 0755)
|
||||||
|
os.MkdirAll(binDir, 0755)
|
||||||
|
|
||||||
|
return &Installer{
|
||||||
|
installDir: installDir,
|
||||||
|
binDir: binDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintPathInstructions zeigt dem Benutzer, wie der PATH gesetzt wird
|
||||||
|
func (i *Installer) PrintPathInstructions() {
|
||||||
|
fmt.Println("\n📁 LSP-Server werden installiert in:")
|
||||||
|
fmt.Printf(" Binaries: %s\n", i.binDir)
|
||||||
|
fmt.Printf(" Data: %s\n\n", i.installDir)
|
||||||
|
|
||||||
|
// Prüfen ob bereits im PATH
|
||||||
|
pathEnv := os.Getenv("PATH")
|
||||||
|
pathList := filepath.SplitList(pathEnv)
|
||||||
|
inPath := false
|
||||||
|
for _, p := range pathList {
|
||||||
|
if p == i.binDir {
|
||||||
|
inPath = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inPath {
|
||||||
|
fmt.Println("⚠️ Das Binary-Verzeichnis ist noch nicht im PATH!")
|
||||||
|
fmt.Println("\nFüge folgende Zeile zu deiner Shell-Konfiguration hinzu:")
|
||||||
|
fmt.Printf("\n # Für Bash (~/.bashrc):\n")
|
||||||
|
fmt.Printf(" export PATH=\"%s:$PATH\"\n\n", i.binDir)
|
||||||
|
fmt.Printf(" # Für Zsh (~/.zshrc):\n")
|
||||||
|
fmt.Printf(" export PATH=\"%s:$PATH\"\n\n", i.binDir)
|
||||||
|
fmt.Printf(" # Für Fish (~/.config/fish/config.fish):\n")
|
||||||
|
fmt.Printf(" set -x PATH %s $PATH\n\n", i.binDir)
|
||||||
|
} else {
|
||||||
|
fmt.Println("✅ Binary-Verzeichnis ist bereits im PATH!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install installiert einen LSP-Server basierend auf dem Typ
|
||||||
|
func (i *Installer) Install(server LSPServer) error {
|
||||||
|
switch server.InstallType {
|
||||||
|
case "go", "golang":
|
||||||
|
return i.installGo(server)
|
||||||
|
case "npm":
|
||||||
|
return i.installNpm(server)
|
||||||
|
case "github":
|
||||||
|
return i.installGithub(server)
|
||||||
|
case "cargo":
|
||||||
|
return i.installCargo(server)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported install type: %s (für %s)", server.InstallType, server.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// installGo installiert einen LSP-Server via go install
|
||||||
|
func (i *Installer) installGo(server LSPServer) error {
|
||||||
|
// Mason Source format: "golang.org/x/tools/gopls" oder ähnlich
|
||||||
|
source := server.Source
|
||||||
|
if !strings.Contains(source, "@") {
|
||||||
|
source = source + "@" + server.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "install", source)
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"GOBIN="+i.binDir,
|
||||||
|
)
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("go install failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// installNpm installiert einen LSP-Server via npm
|
||||||
|
func (i *Installer) installNpm(server LSPServer) error {
|
||||||
|
serverDir := filepath.Join(i.installDir, server.Name)
|
||||||
|
os.MkdirAll(serverDir, 0755)
|
||||||
|
|
||||||
|
// Mason Source format: "package-name" (einfacher npm Package-Name)
|
||||||
|
cmd := exec.Command("npm", "install", "-g", "--prefix", serverDir, server.Source)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("npm install failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symlink zum bin-Verzeichnis erstellen
|
||||||
|
// npm installiert in serverDir/lib/node_modules/.bin/
|
||||||
|
binPath := filepath.Join(serverDir, "lib", "node_modules", ".bin", server.Executable)
|
||||||
|
linkPath := filepath.Join(i.binDir, server.Executable)
|
||||||
|
|
||||||
|
os.Remove(linkPath) // Alten Link entfernen
|
||||||
|
return os.Symlink(binPath, linkPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installGithub lädt ein Binary von GitHub Releases herunter
|
||||||
|
func (i *Installer) installGithub(server LSPServer) error {
|
||||||
|
// Mason Source format: "owner/repo"
|
||||||
|
// Wir müssen die Download-URL konstruieren (vereinfacht)
|
||||||
|
downloadURL := fmt.Sprintf("https://github.com/%s/releases/latest/download/%s",
|
||||||
|
server.Source, server.Executable)
|
||||||
|
|
||||||
|
resp, err := http.Get(downloadURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("download failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("download failed with status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(i.binDir, server.Executable)
|
||||||
|
file, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executable-Rechte setzen
|
||||||
|
return os.Chmod(targetPath, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installCargo installiert einen LSP-Server via cargo
|
||||||
|
func (i *Installer) installCargo(server LSPServer) error {
|
||||||
|
serverDir := filepath.Join(i.installDir, server.Name)
|
||||||
|
|
||||||
|
// Mason Source format: "crate-name"
|
||||||
|
cmd := exec.Command("cargo", "install",
|
||||||
|
server.Source,
|
||||||
|
"--root", serverDir)
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cargo install failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symlink erstellen
|
||||||
|
binPath := filepath.Join(serverDir, "bin", server.Executable)
|
||||||
|
linkPath := filepath.Join(i.binDir, server.Executable)
|
||||||
|
|
||||||
|
os.Remove(linkPath)
|
||||||
|
return os.Symlink(binPath, linkPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInstalled prüft, ob ein Server installiert ist
|
||||||
|
func (i *Installer) IsInstalled(server LSPServer) bool {
|
||||||
|
binPath := filepath.Join(i.binDir, server.Executable)
|
||||||
|
_, err := os.Stat(binPath)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninstall entfernt einen installierten LSP-Server
|
||||||
|
func (i *Installer) Uninstall(server LSPServer) error {
|
||||||
|
binPath := filepath.Join(i.binDir, server.Executable)
|
||||||
|
serverDir := filepath.Join(i.installDir, server.Name)
|
||||||
|
|
||||||
|
os.Remove(binPath)
|
||||||
|
os.RemoveAll(serverDir)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
246
main.go
Normal file
246
main.go
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"path/filepath"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/list"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
var (
|
||||||
|
titleStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
|
MarginLeft(2)
|
||||||
|
|
||||||
|
selectedStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#7D56F4"))
|
||||||
|
|
||||||
|
installedBadge = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#04B575")).
|
||||||
|
Render(" [INSTALLED]")
|
||||||
|
)
|
||||||
|
|
||||||
|
// serverItem implementiert list.Item für Bubbletea
|
||||||
|
type serverItem struct {
|
||||||
|
server LSPServer
|
||||||
|
installed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i serverItem) FilterValue() string {
|
||||||
|
return i.server.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i serverItem) Title() string {
|
||||||
|
title := i.server.Name
|
||||||
|
if i.installed {
|
||||||
|
title += installedBadge
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i serverItem) Description() string {
|
||||||
|
return fmt.Sprintf("%s | Languages: %s",
|
||||||
|
i.server.Description,
|
||||||
|
strings.Join(i.server.Languages, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model für die Bubbletea-Anwendung
|
||||||
|
type model struct {
|
||||||
|
list list.Model
|
||||||
|
registry *Registry
|
||||||
|
installer *Installer
|
||||||
|
status string
|
||||||
|
quitting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// installMsg wird nach erfolgreicher Installation gesendet
|
||||||
|
type installMsg struct {
|
||||||
|
server LSPServer
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateMsg wird nach Registry-Update gesendet
|
||||||
|
type updateMsg struct {
|
||||||
|
registry *Registry
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialisiert das Model
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update behandelt Nachrichten und aktualisiert das Model
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case "u":
|
||||||
|
// Registry aktualisieren (force update)
|
||||||
|
m.status = "Updating registry from Mason..."
|
||||||
|
return m, func() tea.Msg {
|
||||||
|
registry, err := LoadRegistryFromMason()
|
||||||
|
return updateMsg{registry: registry, err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "enter":
|
||||||
|
// Server installieren/deinstallieren
|
||||||
|
selectedItem := m.list.SelectedItem().(serverItem)
|
||||||
|
|
||||||
|
if selectedItem.installed {
|
||||||
|
// Deinstallieren
|
||||||
|
m.status = fmt.Sprintf("Uninstalling %s...", selectedItem.server.Name)
|
||||||
|
return m, func() tea.Msg {
|
||||||
|
err := m.installer.Uninstall(selectedItem.server)
|
||||||
|
return installMsg{server: selectedItem.server, err: err}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Installieren
|
||||||
|
m.status = fmt.Sprintf("Installing %s...", selectedItem.server.Name)
|
||||||
|
return m, func() tea.Msg {
|
||||||
|
err := m.installer.Install(selectedItem.server)
|
||||||
|
return installMsg{server: selectedItem.server, err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "i":
|
||||||
|
// Info anzeigen
|
||||||
|
selectedItem := m.list.SelectedItem().(serverItem)
|
||||||
|
m.status = fmt.Sprintf("Server: %s | Homepage: %s | License: %s | Type: %s",
|
||||||
|
selectedItem.server.Name,
|
||||||
|
selectedItem.server.Homepage,
|
||||||
|
selectedItem.server.License,
|
||||||
|
selectedItem.server.InstallType)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case installMsg:
|
||||||
|
// Installation abgeschlossen
|
||||||
|
if msg.err != nil {
|
||||||
|
m.status = fmt.Sprintf("❌ Error: %v", msg.err)
|
||||||
|
} else {
|
||||||
|
m.status = fmt.Sprintf("✅ Successfully installed %s", msg.server.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liste aktualisieren
|
||||||
|
items := m.buildListItems()
|
||||||
|
m.list.SetItems(items)
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case updateMsg:
|
||||||
|
// Registry-Update abgeschlossen
|
||||||
|
if msg.err != nil {
|
||||||
|
m.status = fmt.Sprintf("❌ Registry update failed: %v", msg.err)
|
||||||
|
} else {
|
||||||
|
m.registry = msg.registry
|
||||||
|
m.status = fmt.Sprintf("✅ Registry updated with %d LSP servers", len(msg.registry.Servers))
|
||||||
|
// Liste aktualisieren
|
||||||
|
items := m.buildListItems()
|
||||||
|
m.list.SetItems(items)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.list.SetWidth(msg.Width)
|
||||||
|
m.list.SetHeight(msg.Height - 5)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.list, cmd = m.list.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// View rendert die UI
|
||||||
|
func (m model) View() string {
|
||||||
|
if m.quitting {
|
||||||
|
return "Goodbye! 👋\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
header := titleStyle.Render("🚀 LSP Server Manager (Mason Registry)")
|
||||||
|
|
||||||
|
helpText := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#626262")).
|
||||||
|
Render("\nEnter: Install/Uninstall | i: Info | u: Update Registry | q: Quit | /: Filter")
|
||||||
|
|
||||||
|
statusBar := ""
|
||||||
|
if m.status != "" {
|
||||||
|
statusBar = "\n" + lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
|
Render(m.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s\n\n%s%s%s\n",
|
||||||
|
header,
|
||||||
|
m.list.View(),
|
||||||
|
statusBar,
|
||||||
|
helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildListItems erstellt die Liste der Server-Items
|
||||||
|
func (m *model) buildListItems() []list.Item {
|
||||||
|
items := make([]list.Item, len(m.registry.Servers))
|
||||||
|
|
||||||
|
for i, server := range m.registry.Servers {
|
||||||
|
items[i] = serverItem{
|
||||||
|
server: server,
|
||||||
|
installed: m.installer.IsInstalled(server),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hauptfunktion
|
||||||
|
func main() {
|
||||||
|
// Registry laden - verwende "mason" um von GitHub zu laden
|
||||||
|
// oder einen Dateipfad für lokale registry.json
|
||||||
|
registry, err := LoadRegistry("mason")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error loading registry: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Loaded %d LSP servers from registry\n\n", len(registry.Servers))
|
||||||
|
|
||||||
|
// Installer erstellen
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
installDir := filepath.Join(homeDir, ".lsp-manager")
|
||||||
|
installer := NewInstaller(installDir)
|
||||||
|
installer.PrintPathInstructions()
|
||||||
|
|
||||||
|
// Liste erstellen
|
||||||
|
delegate := list.NewDefaultDelegate()
|
||||||
|
delegate.Styles.SelectedTitle = selectedStyle
|
||||||
|
delegate.Styles.SelectedDesc = selectedStyle
|
||||||
|
|
||||||
|
initialModel := model{
|
||||||
|
registry: registry,
|
||||||
|
installer: installer,
|
||||||
|
}
|
||||||
|
|
||||||
|
items := initialModel.buildListItems()
|
||||||
|
initialModel.list = list.New(items, delegate, 0, 0)
|
||||||
|
initialModel.list.Title = "Available LSP Servers"
|
||||||
|
initialModel.list.SetFilteringEnabled(true)
|
||||||
|
|
||||||
|
// Bubbletea-Programm starten
|
||||||
|
p := tea.NewProgram(initialModel, tea.WithAltScreen())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
289
registry.go
Normal file
289
registry.go
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MASON_REGISTRY_RELEASE = "https://github.com/mason-org/mason-registry/releases/latest/download/registry.json.zip"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LSPServer repräsentiert einen Language Server
|
||||||
|
type LSPServer struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Homepage string `json:"homepage"`
|
||||||
|
Languages []string `json:"languages"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Executable string `json:"executable"`
|
||||||
|
InstallType string `json:"install_type"` // "github", "npm", "cargo", "go"
|
||||||
|
Source string `json:"source"` // URL oder Package-Name
|
||||||
|
Version string `json:"version"`
|
||||||
|
License string `json:"license"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry verwaltet alle verfügbaren LSP-Server
|
||||||
|
type Registry struct {
|
||||||
|
Servers []LSPServer `json:"servers"`
|
||||||
|
CachePath string
|
||||||
|
LastUpdated time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// MasonPackage entspricht der Mason-Registry Struktur
|
||||||
|
type MasonPackage struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Homepage string `json:"homepage"`
|
||||||
|
Licenses []string `json:"licenses"`
|
||||||
|
Languages []string `json:"languages"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Source struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"source"`
|
||||||
|
Bin map[string]string `json:"bin,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadRegistry lädt die Registry aus einer JSON-Datei oder von Mason
|
||||||
|
func LoadRegistry(path string) (*Registry, error) {
|
||||||
|
// Prüfe ob path "mason" ist -> dann von GitHub laden
|
||||||
|
if path == "mason" {
|
||||||
|
return LoadRegistryFromMason()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ansonsten aus lokaler Datei laden
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry Registry
|
||||||
|
if err := json.Unmarshal(data, ®istry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ®istry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadRegistryFromMason lädt die Registry von Mason mit Cache
|
||||||
|
func LoadRegistryFromMason() (*Registry, error) {
|
||||||
|
cacheDir := filepath.Join(xdg.CacheHome, "lsp-manager")
|
||||||
|
os.MkdirAll(cacheDir, 0755)
|
||||||
|
cachePath := filepath.Join(cacheDir, "mason-registry.json")
|
||||||
|
|
||||||
|
registry := &Registry{
|
||||||
|
CachePath: cachePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Cache existiert und aktuell ist (< 24h alt)
|
||||||
|
if info, err := os.Stat(cachePath); err == nil {
|
||||||
|
if time.Since(info.ModTime()) < 24*time.Hour {
|
||||||
|
fmt.Println("📦 Loading registry from cache...")
|
||||||
|
if err := registry.loadFromCache(); err == nil {
|
||||||
|
return registry, nil
|
||||||
|
}
|
||||||
|
fmt.Printf("Cache load failed, downloading fresh copy...\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache ist veraltet oder existiert nicht, lade von GitHub
|
||||||
|
if err := registry.downloadFromMason(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadFromCache lädt die Registry aus dem lokalen Cache
|
||||||
|
func (r *Registry) loadFromCache() error {
|
||||||
|
data, err := os.ReadFile(r.CachePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &r.Servers); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloadFromMason lädt die Mason-Registry von GitHub
|
||||||
|
func (r *Registry) downloadFromMason() error {
|
||||||
|
fmt.Println("📦 Downloading Mason registry from GitHub...")
|
||||||
|
|
||||||
|
resp, err := http.Get(MASON_REGISTRY_RELEASE)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download registry: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lese ZIP-Daten in Speicher
|
||||||
|
zipData, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read registry data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entpacke ZIP
|
||||||
|
zipReader, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open zip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finde registry.json in ZIP
|
||||||
|
var registryJSON []byte
|
||||||
|
for _, file := range zipReader.File {
|
||||||
|
if file.Name == "registry.json" {
|
||||||
|
f, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open registry.json: %w", err)
|
||||||
|
}
|
||||||
|
registryJSON, err = io.ReadAll(f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read registry.json: %w", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if registryJSON == nil {
|
||||||
|
return fmt.Errorf("registry.json not found in archive")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Mason-Format: registry.json ist ein Array
|
||||||
|
var masonPackages []MasonPackage
|
||||||
|
if err := json.Unmarshal(registryJSON, &masonPackages); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konvertiere zu unserem Format
|
||||||
|
r.Servers = convertMasonToLSPServers(masonPackages)
|
||||||
|
|
||||||
|
fmt.Printf("✅ Registry updated with %d packages (%d LSP servers)\n",
|
||||||
|
len(masonPackages), len(r.Servers))
|
||||||
|
|
||||||
|
// Speichere konvertierte Servers im Cache
|
||||||
|
if cacheData, err := json.MarshalIndent(r.Servers, "", " "); err == nil {
|
||||||
|
if err := os.WriteFile(r.CachePath, cacheData, 0644); err != nil {
|
||||||
|
fmt.Printf("⚠️ Warning: failed to cache registry: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.LastUpdated = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertMasonToLSPServers(masonPackages []MasonPackage) []LSPServer {
|
||||||
|
servers := make([]LSPServer, 0, len(masonPackages))
|
||||||
|
|
||||||
|
for _, pkg := range masonPackages {
|
||||||
|
// Filtere nur LSP-Server
|
||||||
|
isLSP := false
|
||||||
|
for _, cat := range pkg.Categories {
|
||||||
|
if cat == "LSP" {
|
||||||
|
isLSP = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isLSP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bestimme Executable-Namen
|
||||||
|
executable := pkg.Name
|
||||||
|
if len(pkg.Bin) > 0 {
|
||||||
|
for _, binName := range pkg.Bin {
|
||||||
|
executable = binName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konvertiere Source-ID zu unserem Format
|
||||||
|
installType, source := parseMasonSourceID(pkg.Source.ID)
|
||||||
|
|
||||||
|
servers = append(servers, LSPServer{
|
||||||
|
Name: pkg.Name,
|
||||||
|
Description: pkg.Description,
|
||||||
|
Homepage: pkg.Homepage,
|
||||||
|
Languages: pkg.Languages,
|
||||||
|
Categories: pkg.Categories,
|
||||||
|
Executable: executable,
|
||||||
|
InstallType: installType,
|
||||||
|
Source: source,
|
||||||
|
Version: "latest",
|
||||||
|
License: getLicense(pkg.Licenses),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMasonSourceID extrahiert InstallType und Source aus Mason Source-ID
|
||||||
|
// Format: "pkg:github/owner/repo", "pkg:npm/package-name", etc.
|
||||||
|
func parseMasonSourceID(sourceID string) (installType, source string) {
|
||||||
|
if !strings.HasPrefix(sourceID, "pkg:") {
|
||||||
|
return "unknown", sourceID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entferne "pkg:" Prefix
|
||||||
|
parts := strings.SplitN(sourceID[4:], "/", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return "unknown", sourceID
|
||||||
|
}
|
||||||
|
|
||||||
|
installType = parts[0]
|
||||||
|
source = parts[1]
|
||||||
|
|
||||||
|
return installType, source
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLicense gibt die erste Lizenz zurück oder "Unknown"
|
||||||
|
func getLicense(licenses []string) string {
|
||||||
|
if len(licenses) > 0 {
|
||||||
|
return licenses[0]
|
||||||
|
}
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadRegistryFromURL lädt von einer Remote-URL (Fallback)
|
||||||
|
func LoadRegistryFromURL(url string) (*Registry, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry Registry
|
||||||
|
if err := json.Unmarshal(data, ®istry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ®istry, nil
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue