feat: initial commit

This commit is contained in:
Patryk Hegenberg 2025-10-20 10:40:40 +02:00
commit 8a467d9004
5 changed files with 828 additions and 0 deletions

289
registry.go Normal file
View 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, &registry); err != nil {
return nil, err
}
return &registry, 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, &registry); err != nil {
return nil, err
}
return &registry, nil
}