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 }