mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-08 06:25:37 -05:00
257 lines
5.6 KiB
Go
257 lines
5.6 KiB
Go
package plugins
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-git/go-git/v6"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
const registryRepo = "https://github.com/AvengeMedia/dms-plugin-registry.git"
|
|
|
|
type Plugin struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Capabilities []string `json:"capabilities"`
|
|
Category string `json:"category"`
|
|
Repo string `json:"repo"`
|
|
Path string `json:"path,omitempty"`
|
|
Author string `json:"author"`
|
|
Description string `json:"description"`
|
|
Dependencies []string `json:"dependencies,omitempty"`
|
|
Compositors []string `json:"compositors"`
|
|
Distro []string `json:"distro"`
|
|
Screenshot string `json:"screenshot,omitempty"`
|
|
}
|
|
|
|
type GitClient interface {
|
|
PlainClone(path string, url string) error
|
|
Pull(path string) error
|
|
HasUpdates(path string) (bool, error)
|
|
}
|
|
|
|
type realGitClient struct{}
|
|
|
|
func (g *realGitClient) PlainClone(path string, url string) error {
|
|
_, err := git.PlainClone(path, &git.CloneOptions{
|
|
URL: url,
|
|
Progress: os.Stdout,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (g *realGitClient) Pull(path string) error {
|
|
repo, err := git.PlainOpen(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
worktree, err := repo.Worktree()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = worktree.Pull(&git.PullOptions{})
|
|
if err != nil && err.Error() != "already up-to-date" {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *realGitClient) HasUpdates(path string) (bool, error) {
|
|
repo, err := git.PlainOpen(path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Fetch remote changes
|
|
err = repo.Fetch(&git.FetchOptions{})
|
|
if err != nil && err.Error() != "already up-to-date" {
|
|
// If fetch fails, we can't determine if there are updates
|
|
// Return false and the error
|
|
return false, err
|
|
}
|
|
|
|
// Get the HEAD reference
|
|
head, err := repo.Head()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
|
|
remote, err := repo.Remote("origin")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
refs, err := remote.List(&git.ListOptions{})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Find the default branch remote ref
|
|
var remoteHead string
|
|
for _, ref := range refs {
|
|
if ref.Name().IsBranch() {
|
|
// Try common branch names
|
|
if ref.Name().Short() == "main" || ref.Name().Short() == "master" {
|
|
remoteHead = ref.Hash().String()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we couldn't find a remote HEAD, assume no updates
|
|
if remoteHead == "" {
|
|
return false, nil
|
|
}
|
|
|
|
// Compare local HEAD with remote HEAD
|
|
return head.Hash().String() != remoteHead, nil
|
|
}
|
|
|
|
type Registry struct {
|
|
fs afero.Fs
|
|
cacheDir string
|
|
plugins []Plugin
|
|
git GitClient
|
|
}
|
|
|
|
func NewRegistry() (*Registry, error) {
|
|
return NewRegistryWithFs(afero.NewOsFs())
|
|
}
|
|
|
|
func NewRegistryWithFs(fs afero.Fs) (*Registry, error) {
|
|
cacheDir := getCacheDir()
|
|
return &Registry{
|
|
fs: fs,
|
|
cacheDir: cacheDir,
|
|
git: &realGitClient{},
|
|
}, nil
|
|
}
|
|
|
|
func getCacheDir() string {
|
|
return filepath.Join(os.TempDir(), "dankdots-plugin-registry")
|
|
}
|
|
|
|
func (r *Registry) Update() error {
|
|
exists, err := afero.DirExists(r.fs, r.cacheDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check cache directory: %w", err)
|
|
}
|
|
|
|
if !exists {
|
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
|
}
|
|
|
|
if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
|
|
return fmt.Errorf("failed to clone registry: %w", err)
|
|
}
|
|
} else {
|
|
// Try to pull, if it fails (e.g., shallow clone corruption), delete and re-clone
|
|
if err := r.git.Pull(r.cacheDir); err != nil {
|
|
// Repository is likely corrupted or has issues, delete and re-clone
|
|
if err := r.fs.RemoveAll(r.cacheDir); err != nil {
|
|
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
|
}
|
|
|
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
|
}
|
|
|
|
if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
|
|
return fmt.Errorf("failed to re-clone registry: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return r.loadPlugins()
|
|
}
|
|
|
|
func (r *Registry) loadPlugins() error {
|
|
pluginsDir := filepath.Join(r.cacheDir, "plugins")
|
|
|
|
entries, err := afero.ReadDir(r.fs, pluginsDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read plugins directory: %w", err)
|
|
}
|
|
|
|
r.plugins = []Plugin{}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
|
|
continue
|
|
}
|
|
|
|
data, err := afero.ReadFile(r.fs, filepath.Join(pluginsDir, entry.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var plugin Plugin
|
|
if err := json.Unmarshal(data, &plugin); err != nil {
|
|
continue
|
|
}
|
|
|
|
if plugin.ID == "" {
|
|
plugin.ID = strings.TrimSuffix(entry.Name(), ".json")
|
|
}
|
|
|
|
r.plugins = append(r.plugins, plugin)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) List() ([]Plugin, error) {
|
|
if len(r.plugins) == 0 {
|
|
if err := r.Update(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return SortByFirstParty(r.plugins), nil
|
|
}
|
|
|
|
func (r *Registry) Search(query string) ([]Plugin, error) {
|
|
allPlugins, err := r.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if query == "" {
|
|
return allPlugins, nil
|
|
}
|
|
|
|
return SortByFirstParty(FuzzySearch(query, allPlugins)), nil
|
|
}
|
|
|
|
func (r *Registry) Get(idOrName string) (*Plugin, error) {
|
|
plugins, err := r.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// First, try to find by ID (preferred method)
|
|
for _, p := range plugins {
|
|
if p.ID == idOrName {
|
|
return &p, nil
|
|
}
|
|
}
|
|
|
|
// Fallback to name for backward compatibility
|
|
for _, p := range plugins {
|
|
if p.Name == idOrName {
|
|
return &p, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("plugin not found: %s", idOrName)
|
|
}
|