chore: import upstream snapshot with attribution
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:13 +08:00
commit ead81af521
414 changed files with 73946 additions and 0 deletions
+401
View File
@@ -0,0 +1,401 @@
// Package pluginmgr implements the `cliamp plugins` CLI subcommands:
// list, install, and remove.
package pluginmgr
import (
"bufio"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
lua "github.com/yuin/gopher-lua"
"github.com/bjarneo/cliamp/internal/appdir"
"github.com/bjarneo/cliamp/internal/fileutil"
"github.com/bjarneo/cliamp/internal/plugintrust"
"github.com/bjarneo/cliamp/luaplugin"
)
var httpClient = &http.Client{Timeout: 30 * time.Second}
const maxPluginSize = 1 << 20 // 1 MB
const metadataTimeout = 250 * time.Millisecond
var (
input io.Reader = os.Stdin
output io.Writer = os.Stdout
)
// pluginInfo holds metadata extracted from a plugin's register() call.
type pluginInfo struct {
file string
name string
version string
description string
typ string
permissions []string
path string
trust string
err error
}
// List prints all installed plugins with their metadata.
func List() error {
dir, err := appdir.PluginDir()
if err != nil {
return err
}
plugins, err := scanPlugins(dir)
if err != nil {
fmt.Println("No plugins installed.")
return nil
}
if len(plugins) == 0 {
fmt.Println("No plugins installed.")
return nil
}
manifest, trustErr := plugintrust.Load(dir)
if trustErr != nil {
return trustErr
}
for i := range plugins {
switch err := plugintrust.Verify(manifest, strings.TrimSuffix(plugins[i].file, "/"), plugins[i].path); {
case err == nil:
plugins[i].trust = "trusted"
case err == plugintrust.ErrHashMismatch:
plugins[i].trust = "changed"
default:
plugins[i].trust = "untrusted"
}
}
// Calculate column widths.
nameW, typeW, verW := 4, 4, 7 // "NAME", "TYPE", "VERSION"
for _, p := range plugins {
if len(p.name) > nameW {
nameW = len(p.name)
}
if len(p.typ) > typeW {
typeW = len(p.typ)
}
if len(p.version) > verW {
verW = len(p.version)
}
}
fmt.Fprintf(output, "%-*s %-*s %-*s %-9s %s\n", nameW, "NAME", typeW, "TYPE", verW, "VERSION", "TRUST", "DESCRIPTION")
for _, p := range plugins {
fmt.Fprintf(output, "%-*s %-*s %-*s %-9s %s\n", nameW, p.name, typeW, p.typ, verW, p.version, p.trust, p.description)
}
return nil
}
// Install downloads a plugin from the given source and saves it to the plugins directory.
func Install(source string, assumeYes ...bool) error {
urls, name, err := resolveSource(source)
if err != nil {
return err
}
if err := validateName(name); err != nil {
return err
}
dir, err := appdir.PluginDir()
if err != nil {
return err
}
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("creating plugins directory: %w", err)
}
if err := os.Chmod(dir, 0o700); err != nil {
return fmt.Errorf("securing plugins directory: %w", err)
}
// Check if already installed (file or directory).
dest := filepath.Join(dir, name+".lua")
if _, err := os.Stat(dest); err == nil {
return fmt.Errorf("plugin %q already exists at %s (remove it first with: cliamp plugins remove %s)", name, dest, name)
}
if info, err := os.Stat(filepath.Join(dir, name)); err == nil && info.IsDir() {
return fmt.Errorf("plugin %q already exists as directory (remove it first with: cliamp plugins remove %s)", name, name)
}
// Try each candidate URL.
var body []byte
for _, u := range urls {
fmt.Printf("Trying %s...\n", u)
b, err := download(u)
if err == nil {
body = b
break
}
}
if body == nil {
return fmt.Errorf("could not download plugin from any of: %s", strings.Join(urls, ", "))
}
info := extractMetadataSource(string(body))
if info.err != nil {
return fmt.Errorf("inspect plugin metadata: %w", info.err)
}
h := sha256.Sum256(body)
hash := hex.EncodeToString(h[:])
fmt.Fprintf(output, "Source: %s\nSHA-256: %s\nDeclared permissions: %s\nImplicit access: unrestricted reads; allowlisted writes; public HTTP\n",
source, hash, displayPermissions(info.permissions))
yes := len(assumeYes) > 0 && assumeYes[0]
if !yes {
fmt.Fprint(output, "Trust and install this plugin? [y/N] ")
answer, err := bufio.NewReader(input).ReadString('\n')
if err != nil && len(answer) == 0 {
return errors.New("approval required; rerun with --yes for non-interactive installation")
}
answer = strings.ToLower(strings.TrimSpace(answer))
if answer != "y" && answer != "yes" {
return errors.New("plugin installation not approved")
}
}
if err := fileutil.WriteFileAtomic(dest, body, 0o600); err != nil {
return fmt.Errorf("writing plugin: %w", err)
}
if _, err := plugintrust.Approve(dir, name, dest); err != nil {
_ = os.Remove(dest)
return fmt.Errorf("recording plugin trust: %w", err)
}
fmt.Printf("Installed %s → %s\n", name, dest)
return nil
}
// Trust approves the current contents of an installed plugin.
func Trust(name string, assumeYes bool) error {
if err := validateName(name); err != nil {
return err
}
dir, err := appdir.PluginDir()
if err != nil {
return err
}
path := filepath.Join(dir, name+".lua")
if st, statErr := os.Stat(path); statErr != nil {
path = filepath.Join(dir, name, "init.lua")
} else if st.IsDir() {
return fmt.Errorf("plugin %q not found", name)
}
info := extractMetadata(path)
if info.err != nil {
return fmt.Errorf("inspect plugin metadata: %w", info.err)
}
hash, err := plugintrust.HashFile(path)
if err != nil {
return err
}
fmt.Fprintf(output, "Plugin: %s\nSHA-256: %s\nDeclared permissions: %s\nImplicit access: unrestricted reads; allowlisted writes; public HTTP\n",
name, hash, displayPermissions(info.permissions))
if !assumeYes {
fmt.Fprint(output, "Trust this plugin content? [y/N] ")
answer, readErr := bufio.NewReader(input).ReadString('\n')
if readErr != nil && len(answer) == 0 {
return errors.New("approval required; rerun with --yes")
}
answer = strings.ToLower(strings.TrimSpace(answer))
if answer != "y" && answer != "yes" {
return errors.New("plugin trust not approved")
}
}
_, err = plugintrust.Approve(dir, name, path)
return err
}
func validateName(name string) error {
if name == "" || name == "." || name == ".." || filepath.Base(name) != name || strings.ContainsAny(name, `/\\`) {
return fmt.Errorf("invalid plugin name %q", name)
}
return nil
}
func displayPermissions(perms []string) string {
if len(perms) == 0 {
return "none"
}
return strings.Join(perms, ", ")
}
// Remove deletes a plugin by name.
func Remove(name string) error {
if err := validateName(name); err != nil {
return err
}
dir, err := appdir.PluginDir()
if err != nil {
return err
}
// Try single file first, then directory.
filePath := filepath.Join(dir, name+".lua")
if _, err := os.Stat(filePath); err == nil {
if err := os.Remove(filePath); err != nil {
return fmt.Errorf("removing plugin: %w", err)
}
fmt.Printf("Removed %s\n", filePath)
return nil
}
dirPath := filepath.Join(dir, name)
if info, err := os.Stat(dirPath); err == nil && info.IsDir() {
if err := os.RemoveAll(dirPath); err != nil {
return fmt.Errorf("removing plugin directory: %w", err)
}
fmt.Printf("Removed %s\n", dirPath)
return nil
}
return fmt.Errorf("plugin %q not found", name)
}
func download(url string) ([]byte, error) {
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %s", resp.Status)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, maxPluginSize+1))
if err != nil {
return nil, err
}
if len(body) > maxPluginSize {
return nil, fmt.Errorf("plugin too large (max %d bytes)", maxPluginSize)
}
if len(body) == 0 {
return nil, fmt.Errorf("empty response body")
}
return body, nil
}
// scanPlugins reads the plugin directory and extracts metadata from each plugin
// using a lightweight Lua VM.
func scanPlugins(dir string) ([]pluginInfo, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var plugins []pluginInfo
for _, e := range entries {
var path, file string
if e.IsDir() {
init := filepath.Join(dir, e.Name(), "init.lua")
if _, err := os.Stat(init); err != nil {
continue
}
path = init
file = e.Name() + "/"
} else if strings.HasSuffix(e.Name(), ".lua") {
path = filepath.Join(dir, e.Name())
file = e.Name()
} else {
continue
}
info := extractMetadata(path)
info.file = file
info.path = path
if info.name == "" {
info.name = strings.TrimSuffix(e.Name(), ".lua")
}
plugins = append(plugins, info)
}
return plugins, nil
}
// extractMetadata runs a Lua file in a minimal VM to capture the plugin.register() call.
func extractMetadata(path string) pluginInfo {
data, err := os.ReadFile(path)
if err != nil {
return pluginInfo{err: err}
}
return extractMetadataSource(string(data))
}
func extractMetadataSource(source string) pluginInfo {
L := lua.NewState(lua.Options{SkipOpenLibs: false})
defer L.Close()
// Apply the same sandbox as the runtime: extractMetadata runs DoFile on
// the whole plugin file (not just register()), so top-level code must not
// have access to os.execute/io/dofile when merely listing plugins.
luaplugin.Sandbox(L)
var info pluginInfo
ctx, cancel := context.WithTimeout(context.Background(), metadataTimeout)
defer cancel()
L.SetContext(ctx)
defer L.RemoveContext()
// Stub out plugin.register() to capture metadata without side effects.
pluginTbl := L.NewTable()
L.SetField(pluginTbl, "register", L.NewFunction(func(L *lua.LState) int {
opts := L.CheckTable(1)
if v := opts.RawGetString("name"); v != lua.LNil {
info.name = v.String()
}
if v := opts.RawGetString("version"); v != lua.LNil {
info.version = v.String()
}
if v := opts.RawGetString("description"); v != lua.LNil {
info.description = v.String()
}
if v := opts.RawGetString("type"); v != lua.LNil {
info.typ = v.String()
}
if v := opts.RawGetString("permissions"); v != lua.LNil {
tbl, ok := v.(*lua.LTable)
if !ok {
info.err = errors.New("permissions must be an array")
} else {
known := map[string]bool{"control": true, "exec": true, "keymap": true}
tbl.ForEach(func(_, value lua.LValue) {
permission := value.String()
if !known[permission] && info.err == nil {
info.err = fmt.Errorf("unknown permission %q", permission)
}
info.permissions = append(info.permissions, permission)
})
}
}
// Return a dummy object with stub on/config methods.
obj := L.NewTable()
noop := L.NewFunction(func(L *lua.LState) int {
L.Push(lua.LNil)
return 1
})
L.SetField(obj, "on", noop)
L.SetField(obj, "config", noop)
L.Push(obj)
return 1
}))
L.SetGlobal("plugin", pluginTbl)
// No cliamp API is installed: metadata inspection happens before trust.
if err := L.DoString(source); err != nil && info.name == "" {
info.err = err
}
return info
}
+328
View File
@@ -0,0 +1,328 @@
package pluginmgr
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// redirectTransport rewrites every request's Host to point at target.
type redirectTransport struct {
target *url.URL
rt http.RoundTripper
}
func (t redirectTransport) RoundTrip(req *http.Request) (*http.Response, error) {
clone := req.Clone(req.Context())
clone.URL.Scheme = t.target.Scheme
clone.URL.Host = t.target.Host
clone.Host = t.target.Host
return t.rt.RoundTrip(clone)
}
func installTestClient(t *testing.T, serverURL string) {
t.Helper()
u, err := url.Parse(serverURL)
if err != nil {
t.Fatalf("parse: %v", err)
}
old := httpClient
httpClient = &http.Client{
Timeout: 5 * time.Second,
Transport: redirectTransport{target: u, rt: http.DefaultTransport},
}
t.Cleanup(func() { httpClient = old })
}
func withTempHome(t *testing.T) string {
t.Helper()
home := t.TempDir()
t.Setenv("HOME", home)
return home
}
func TestScanPluginsEmptyDir(t *testing.T) {
dir := t.TempDir()
plugins, err := scanPlugins(dir)
if err != nil {
t.Fatalf("scanPlugins: %v", err)
}
if len(plugins) != 0 {
t.Errorf("expected 0 plugins in empty dir, got %d", len(plugins))
}
}
func TestScanPluginsSingleFile(t *testing.T) {
dir := t.TempDir()
src := `plugin.register({
name = "hello",
version = "1.2",
description = "says hi",
type = "visualizer",
})
`
if err := os.WriteFile(filepath.Join(dir, "hello.lua"), []byte(src), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
plugins, err := scanPlugins(dir)
if err != nil {
t.Fatalf("scanPlugins: %v", err)
}
if len(plugins) != 1 {
t.Fatalf("got %d plugins, want 1", len(plugins))
}
got := plugins[0]
if got.name != "hello" {
t.Errorf("name = %q, want hello", got.name)
}
if got.version != "1.2" {
t.Errorf("version = %q, want 1.2", got.version)
}
if got.description != "says hi" {
t.Errorf("description = %q, want 'says hi'", got.description)
}
if got.typ != "visualizer" {
t.Errorf("typ = %q, want visualizer", got.typ)
}
if got.file != "hello.lua" {
t.Errorf("file = %q, want hello.lua", got.file)
}
}
func TestScanPluginsFallsBackToFilename(t *testing.T) {
dir := t.TempDir()
// Plugin without register() call — name should default to filename.
if err := os.WriteFile(filepath.Join(dir, "nameless.lua"), []byte(`-- nothing`), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
plugins, err := scanPlugins(dir)
if err != nil {
t.Fatalf("scanPlugins: %v", err)
}
if len(plugins) != 1 {
t.Fatalf("got %d plugins, want 1", len(plugins))
}
if plugins[0].name != "nameless" {
t.Errorf("name = %q, want 'nameless' (filename fallback)", plugins[0].name)
}
}
func TestScanPluginsDirectoryEntry(t *testing.T) {
dir := t.TempDir()
// Create a directory plugin: <dir>/myplug/init.lua
sub := filepath.Join(dir, "myplug")
if err := os.MkdirAll(sub, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join(sub, "init.lua"), []byte(`plugin.register({ name = "myplug", version = "0.1" })`), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
plugins, err := scanPlugins(dir)
if err != nil {
t.Fatalf("scanPlugins: %v", err)
}
if len(plugins) != 1 {
t.Fatalf("got %d plugins, want 1", len(plugins))
}
if plugins[0].file != "myplug/" {
t.Errorf("file = %q, want 'myplug/'", plugins[0].file)
}
}
func TestScanPluginsIgnoresNonLua(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("nothing"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
plugins, err := scanPlugins(dir)
if err != nil {
t.Fatalf("scanPlugins: %v", err)
}
if len(plugins) != 0 {
t.Errorf("non-lua file should be ignored, got %+v", plugins)
}
}
func TestListNoPlugins(t *testing.T) {
withTempHome(t)
if err := List(); err != nil {
t.Errorf("List on empty dir should not error, got %v", err)
}
}
func TestListPluginsShowsInstalled(t *testing.T) {
home := withTempHome(t)
plugDir := filepath.Join(home, ".config", "cliamp", "plugins")
if err := os.MkdirAll(plugDir, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join(plugDir, "x.lua"), []byte(`plugin.register({ name = "x", version = "1.0" })`), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
// List writes to stdout — we just verify it doesn't error.
if err := List(); err != nil {
t.Errorf("List: %v", err)
}
}
func TestRemoveFile(t *testing.T) {
home := withTempHome(t)
plugDir := filepath.Join(home, ".config", "cliamp", "plugins")
if err := os.MkdirAll(plugDir, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
path := filepath.Join(plugDir, "foo.lua")
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := Remove("foo"); err != nil {
t.Fatalf("Remove: %v", err)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Errorf("plugin should be removed, stat err=%v", err)
}
}
func TestRemoveDirectory(t *testing.T) {
home := withTempHome(t)
plugDir := filepath.Join(home, ".config", "cliamp", "plugins")
nested := filepath.Join(plugDir, "bar", "init.lua")
if err := os.MkdirAll(filepath.Dir(nested), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(nested, []byte("x"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := Remove("bar"); err != nil {
t.Fatalf("Remove: %v", err)
}
if _, err := os.Stat(filepath.Dir(nested)); !os.IsNotExist(err) {
t.Errorf("plugin dir should be removed, stat err=%v", err)
}
}
func TestRemoveMissing(t *testing.T) {
withTempHome(t)
err := Remove("ghost")
if err == nil {
t.Error("Remove non-existent plugin should error")
}
if !strings.Contains(err.Error(), "not found") {
t.Errorf("error = %q, want to mention 'not found'", err.Error())
}
}
func TestInstallFromRawURL(t *testing.T) {
withTempHome(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasSuffix(r.URL.Path, "/example.lua") {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(`plugin.register({ name = "example", version = "1" })`))
}))
defer srv.Close()
installTestClient(t, srv.URL)
if err := Install(srv.URL+"/example.lua", true); err != nil {
t.Fatalf("Install: %v", err)
}
// Verify the installed file exists.
home, _ := os.UserHomeDir()
dest := filepath.Join(home, ".config", "cliamp", "plugins", "example.lua")
if _, err := os.Stat(dest); err != nil {
t.Errorf("installed plugin missing: %v", err)
}
}
func TestInstallAlreadyExists(t *testing.T) {
home := withTempHome(t)
plugDir := filepath.Join(home, ".config", "cliamp", "plugins")
if err := os.MkdirAll(plugDir, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
existing := filepath.Join(plugDir, "mypl.lua")
if err := os.WriteFile(existing, []byte("x"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`-- ok`))
}))
defer srv.Close()
installTestClient(t, srv.URL)
err := Install(srv.URL+"/mypl.lua", true)
if err == nil {
t.Fatal("Install over existing plugin should error")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("error = %q, want to mention 'already exists'", err.Error())
}
}
func TestInstallAllURLsFail(t *testing.T) {
withTempHome(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not found", http.StatusNotFound)
}))
defer srv.Close()
installTestClient(t, srv.URL)
err := Install(srv.URL+"/nonexistent.lua", true)
if err == nil {
t.Error("Install with all failing URLs should error")
}
}
func TestInstallTooLarge(t *testing.T) {
withTempHome(t)
big := strings.Repeat("x", maxPluginSize+1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(big))
}))
defer srv.Close()
installTestClient(t, srv.URL)
err := Install(srv.URL+"/huge.lua", true)
if err == nil {
t.Error("Install of oversized plugin should fail")
}
}
func TestDownloadErrors(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
wantErr string
}{
{"404", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "missing", http.StatusNotFound)
}, "HTTP"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srv := httptest.NewServer(tt.handler)
defer srv.Close()
installTestClient(t, srv.URL)
_, err := download(srv.URL + "/x.lua")
if err == nil {
t.Fatal("download should have errored")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("error = %q, want to contain %q", err.Error(), tt.wantErr)
}
})
}
}
+96
View File
@@ -0,0 +1,96 @@
package pluginmgr
import (
"fmt"
"net/url"
"path"
"strings"
)
// resolveSource parses a plugin source string into a download URL and plugin name.
//
// Supported formats:
//
// user/repo → GitHub (HEAD)
// user/repo@v1.0 → GitHub (tag)
// gitlab:user/repo → GitLab (HEAD)
// gitlab:user/repo@v1.0 → GitLab (tag)
// codeberg:user/repo → Codeberg (main)
// codeberg:user/repo@v1 → Codeberg (tag)
// https://example.com/plugin.lua → raw URL
func resolveSource(source string) (urls []string, name string, err error) {
// Raw URL.
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
u, err := url.Parse(source)
if err != nil {
return nil, "", fmt.Errorf("invalid URL: %w", err)
}
base := path.Base(u.Path)
name = strings.TrimSuffix(base, ".lua")
if name == "" || name == "." || name == "/" {
return nil, "", fmt.Errorf("cannot derive a plugin name from URL %q; it should end in <name>.lua", source)
}
return []string{source}, name, nil
}
// Detect forge prefix.
forge := "github"
repo := source
if prefix, rest, ok := strings.Cut(source, ":"); ok {
switch prefix {
case "gitlab", "codeberg":
forge = prefix
repo = rest
default:
return nil, "", fmt.Errorf("unknown forge prefix %q (supported: gitlab, codeberg)", prefix)
}
}
// Split repo@tag.
ref := ""
if r, tag, ok := strings.Cut(repo, "@"); ok {
repo = r
ref = tag
}
// Validate user/repo format.
parts := strings.SplitN(repo, "/", 3)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil, "", fmt.Errorf("invalid source %q (expected user/repo)", source)
}
name = parts[1]
// Convention: repos are named cliamp-plugin-<name> with entry point <name>.lua.
short := strings.TrimPrefix(name, "cliamp-plugin-")
u, err := buildForgeURL(forge, repo, ref, short)
if err != nil {
return nil, "", err
}
return []string{u}, short, nil
}
func buildForgeURL(forge, repo, ref, entrypoint string) (string, error) {
var base string
switch forge {
case "github":
if ref == "" {
ref = "HEAD"
}
base = fmt.Sprintf("https://raw.githubusercontent.com/%s/%s", repo, ref)
case "gitlab":
if ref == "" {
ref = "HEAD"
}
base = fmt.Sprintf("https://gitlab.com/%s/-/raw/%s", repo, ref)
case "codeberg":
if ref == "" {
base = fmt.Sprintf("https://codeberg.org/%s/raw/branch/main", repo)
} else {
base = fmt.Sprintf("https://codeberg.org/%s/raw/tag/%s", repo, ref)
}
default:
return "", fmt.Errorf("unsupported forge %q", forge)
}
return base + "/" + entrypoint + ".lua", nil
}
+116
View File
@@ -0,0 +1,116 @@
package pluginmgr
import (
"testing"
)
func TestResolveSourceGitHub(t *testing.T) {
urls, name, err := resolveSource("user/cliamp-plugin-cool")
if err != nil {
t.Fatal(err)
}
if name != "cool" {
t.Fatalf("name = %q, want %q", name, "cool")
}
want := "https://raw.githubusercontent.com/user/cliamp-plugin-cool/HEAD/cool.lua"
if len(urls) != 1 || urls[0] != want {
t.Fatalf("urls = %v, want [%s]", urls, want)
}
}
func TestResolveSourceGitHubWithTag(t *testing.T) {
urls, name, err := resolveSource("user/cliamp-plugin-cool@v1.0")
if err != nil {
t.Fatal(err)
}
if name != "cool" {
t.Fatalf("name = %q, want %q", name, "cool")
}
want := "https://raw.githubusercontent.com/user/cliamp-plugin-cool/v1.0/cool.lua"
if urls[0] != want {
t.Fatalf("url = %q, want %q", urls[0], want)
}
}
func TestResolveSourceGitLab(t *testing.T) {
urls, name, err := resolveSource("gitlab:user/cliamp-plugin-vis")
if err != nil {
t.Fatal(err)
}
if name != "vis" {
t.Fatalf("name = %q, want %q", name, "vis")
}
want := "https://gitlab.com/user/cliamp-plugin-vis/-/raw/HEAD/vis.lua"
if urls[0] != want {
t.Fatalf("url = %q, want %q", urls[0], want)
}
}
func TestResolveSourceCodeberg(t *testing.T) {
urls, name, err := resolveSource("codeberg:user/cliamp-plugin-eq")
if err != nil {
t.Fatal(err)
}
if name != "eq" {
t.Fatalf("name = %q, want %q", name, "eq")
}
want := "https://codeberg.org/user/cliamp-plugin-eq/raw/branch/main/eq.lua"
if urls[0] != want {
t.Fatalf("url = %q, want %q", urls[0], want)
}
}
func TestResolveSourceCodebergWithTag(t *testing.T) {
urls, _, err := resolveSource("codeberg:user/cliamp-plugin-eq@v2")
if err != nil {
t.Fatal(err)
}
want := "https://codeberg.org/user/cliamp-plugin-eq/raw/tag/v2/eq.lua"
if urls[0] != want {
t.Fatalf("url = %q, want %q", urls[0], want)
}
}
func TestResolveSourceRawURL(t *testing.T) {
urls, name, err := resolveSource("https://example.com/my-plugin.lua")
if err != nil {
t.Fatal(err)
}
if name != "my-plugin" {
t.Fatalf("name = %q, want %q", name, "my-plugin")
}
if urls[0] != "https://example.com/my-plugin.lua" {
t.Fatalf("url = %q", urls[0])
}
}
func TestResolveSourceInvalid(t *testing.T) {
tests := []string{
"",
"justname",
"too/many/parts",
"/nope",
"bitbucket:user/repo",
}
for _, src := range tests {
_, _, err := resolveSource(src)
if err == nil {
t.Errorf("resolveSource(%q) should return error", src)
}
}
}
func TestResolveSourceNonPrefixedRepo(t *testing.T) {
urls, name, err := resolveSource("user/my-equalizer")
if err != nil {
t.Fatal(err)
}
if name != "my-equalizer" {
t.Fatalf("name = %q, want %q", name, "my-equalizer")
}
want := "https://raw.githubusercontent.com/user/my-equalizer/HEAD/my-equalizer.lua"
if urls[0] != want {
t.Fatalf("url = %q, want %q", urls[0], want)
}
}
+12
View File
@@ -0,0 +1,12 @@
package pluginmgr
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
os.Unsetenv("CLIAMP_CONFIG_DIR")
os.Unsetenv("XDG_CONFIG_HOME")
os.Exit(m.Run())
}