Documentation
¶
Overview ¶
Package plugins discovers user-installed lerd plugins and runs them: a folder holding plugin.yaml and the files it names, each surface served by running one of those files, so a plugin can be written in anything.
Index ¶
- Constants
- Variables
- func ClearFailure(name string)
- func Compatible(constraint string) bool
- func DataDirFor(name string) string
- func Dir() string
- func Emit(event string, payload map[string]string)
- func Env(payload map[string]string) map[string]string
- func Failed(f Failure)
- func IsCatalogName(arg string) bool
- func IsEvent(name string) bool
- func KeepRevoked(name string) error
- func KillProcessGroup(cmd *exec.Cmd)
- func Load() ([]*Plugin, []LoadError)
- func Missing(reqs []string) []string
- func NotificationState(p *Plugin) string
- func OwnProcessGroup(cmd *exec.Cmd)
- func PluginForToken(token string) string
- func Record(f Failure) bool
- func RefreshCatalogOlderThan(age time.Duration) error
- func Remove(name string) error
- func RunWithin(cmd *exec.Cmd, limit time.Duration) error
- func RunsHere(list []string) bool
- func Scaffold(name, dest string) error
- func SetFailureNotifier(fn func(Failure))
- func SetNotifications(name string, allowed bool) error
- func SitePayload(site *config.Site) map[string]string
- func Token(name string) (string, error)
- func UpdateListed(name string) error
- func ValidName(name string) bool
- type Catalog
- type Command
- type Endpoint
- type Failure
- type Feature
- type Listing
- type LoadError
- type MCP
- type MCPTool
- type Manifest
- type Plugin
- func (p *Plugin) CardIcon() string
- func (p *Plugin) Command(run, surface string, args []string, extra map[string]string) (*exec.Cmd, error)
- func (p *Plugin) DocsHTML(fileURL func(rel string) string) (string, error)
- func (p *Plugin) Features() []Feature
- func (p *Plugin) Provide(job, action string, request map[string]string) ([]byte, error)
- func (p *Plugin) Requirements() []string
- func (p *Plugin) Serves(rel string) bool
- func (p *Plugin) Surfaces() []string
- type Revocation
- type RevokedError
- type SiteCommand
- type Source
- type UI
- type UIPage
Constants ¶
const ( EventSiteLinked = "site.linked" EventSiteUnlinked = "site.unlinked" EventSiteSecured = "site.secured" EventSitePHPChanged = "site.php_changed" EventWorktreeAdded = "worktree.added" EventWorktreeRemoved = "worktree.removed" EventServiceStarted = "service.started" EventServiceStopped = "service.stopped" EventLerdStarted = "lerd.started" EventLerdStopped = "lerd.stopped" )
Events a plugin can hook. The list only grows: a hook for a name this binary does not know refuses the plugin, so removing or renaming one breaks plugins.
const ( APILimit = 30 * time.Second MCPLimit = 2 * time.Minute SiteCommandLimit = 30 * time.Minute )
How long each surface lets a plugin's executable run before stopping it. A CLI command has none: it runs in the user's terminal, where Ctrl-C is theirs.
const ( NotificationsNone = "none" // the plugin does not send any NotificationsPending = "pending" // it asks to, and the user has not answered NotificationsAllowed = "allowed" NotificationsDenied = "denied" )
The states of a plugin's notifications, as every surface shows them.
const ( SurfaceCLI = "cli" SurfaceSiteCommand = "site_command" SurfaceHook = "hook" SurfaceMCP = "mcp" SurfaceAPI = "api" )
Surfaces name which part of lerd ran a plugin file, passed as LERD_SURFACE so one script can serve several of them.
const APIURL = "http://127.0.0.1:7073"
APIURL is where a plugin reaches lerd's API, lerd-ui's loopback listener (internal/ui listenAddr).
const APIVersion = 1
APIVersion is the manifest version this binary understands. A plugin asking for a newer one is refused rather than half-loaded.
const HookLog = "hooks.log"
HookLog is the file in a plugin's data dir that collects its hooks' output.
const (
// JobWorktreeDatabase creates and drops a worktree's isolated database.
JobWorktreeDatabase = "worktree_database"
)
Jobs a plugin can take over from lerd by declaring itself in charge of them under provides:. Lerd then hands the job to the plugin instead of doing it.
const ManifestFile = "plugin.yaml"
ManifestFile is the file that makes a directory a plugin.
const PagePath = "/_lerd/plugins/"
PagePath is where a site's pages load plugin page scripts and reach their plugins' endpoints, under the site's own origin.
const SurfaceProvide = "provide"
SurfaceProvide is the surface a plugin's executable runs under when lerd hands it a job.
const TokenHeader = "X-Lerd-Plugin-Token"
TokenHeader carries a plugin's token on its calls to lerd's API.
const UIDir = "ui"
UIDir is the one plugin directory the dashboard serves.
Variables ¶
var CatalogURL = "https://plugins.lerd.sh/index.json"
CatalogURL is the approved plugin listing plugins.lerd.sh publishes from the lerd-env/plugins registry. A var so tests can point it at their own server.
var Guide string
Guide is how to build a plugin, written for an AI assistant: the MCP plugin tool returns it and lerd mcp:inject writes it beside lerd's own skill.
Functions ¶
func ClearFailure ¶
func ClearFailure(name string)
ClearFailure forgets the plugin's failures, once it is removed or replaced.
func Compatible ¶
Compatible reports whether this lerd meets a plugin's `>=X.Y` lower bound, on major and minor; other forms are refused. An untagged or dev build loads all, since it already holds the next release. Parsed here to link no service code.
func DataDirFor ¶
DataDirFor is the plugin's own writable directory, kept outside the plugin so reinstalling or removing the code leaves what it stored alone.
func Emit ¶
Emit runs every plugin's hook for event in turn, the payload as JSON on stdin and as LERD_<KEY>. Output and failure go to its hooks.log, never back to the caller, whose operation already succeeded.
func Failed ¶
func Failed(f Failure)
Failed records f as the plugin's latest failure and tells the user when it is news: a different failure from the last, or the same one an hour later.
func IsCatalogName ¶
IsCatalogName reports whether arg names a catalog plugin rather than a folder or a repository, which is how `install` tells them apart.
func KeepRevoked ¶
KeepRevoked lets a revoked plugin run again because the user chose to keep it. The choice is tied to this revocation, so a later one asks again.
func KillProcessGroup ¶
KillProcessGroup kills the group a started cmd leads. The group id is the leader's pid because OwnProcessGroup asked for a new group.
func Load ¶
Load reads every installed plugin. A broken one is reported and skipped so it never takes the rest of lerd down with it.
func NotificationState ¶
NotificationState is where the plugin stands. It is derived rather than stored, so an install or an update that starts asking is pending on its own, and an answer once given holds across updates.
func OwnProcessGroup ¶
OwnProcessGroup makes cmd the leader of a new process group, so KillProcessGroup can stop it together with everything it spawns.
func PluginForToken ¶
PluginForToken returns the installed plugin the token belongs to, or "".
func Record ¶
Record keeps f as the plugin's latest failure without telling anyone, for a surface where the user is already watching it fail, and reports whether it is news.
func RefreshCatalogOlderThan ¶
RefreshCatalogOlderThan fetches the catalog when the copy lerd holds is older than age.
func Remove ¶
Remove deletes an installed plugin's folder, broken or not, and keeps the data it stored in DataDirFor.
func RunWithin ¶
RunWithin runs cmd for every surface but the CLI and stops it, with all it started, once limit passes; the wait still gives up on a child that left the group holding the output open.
func RunsHere ¶
RunsHere reports whether a plugin listing these platforms runs on this machine; a plugin that lists none runs everywhere lerd does.
func SetFailureNotifier ¶
func SetFailureNotifier(fn func(Failure))
SetFailureNotifier sets how this process tells the user about a new failure.
func SetNotifications ¶
SetNotifications records the user's answer for an installed plugin that asks to send notifications.
func SitePayload ¶
SitePayload is how every surface describes a site to a plugin.
func Token ¶
Token is the plugin's API token: an HMAC of its name under a secret only the user can read, so lerd can tell which plugin made a call without storing a token per plugin. The secret is created on first use.
func UpdateListed ¶
UpdateListed replaces an installed plugin with its currently approved commit. The data it stored is kept.
Types ¶
type Catalog ¶
type Catalog struct {
Plugins []Listing `json:"plugins"`
// Revoked are plugins pulled for a security problem; see Revocation.
Revoked []Revocation `json:"revoked,omitempty"`
}
Catalog is the registry's published index.
func LoadCatalog ¶
LoadCatalog returns the approved plugins, fetching the index when the cached copy is a day old. When the fetch fails a cached copy, however old, still answers, so being offline never hides what is already known.
type Command ¶
type Command struct {
Name string `yaml:"name" json:"name"`
Usage string `yaml:"usage,omitempty" json:"usage,omitempty"`
Run string `yaml:"run" json:"run"`
}
Command is a CLI command, invoked as `lerd <plugin>:<name>`.
type Endpoint ¶
type Endpoint struct {
Path string `yaml:"path" json:"path"`
Methods []string `yaml:"methods,omitempty" json:"methods,omitempty"`
Run string `yaml:"run" json:"run"`
// LoopbackOnly closes the endpoint to remote dashboards unless the user
// turned on remote full access, like lerd's own host-action routes.
LoopbackOnly bool `yaml:"loopback_only,omitempty" json:"loopback_only,omitempty"`
// Timeout raises APILimit for this endpoint, up to ten minutes.
Timeout string `yaml:"timeout,omitempty" json:"timeout,omitempty"`
}
Endpoint is served at /api/plugins/<plugin>/<path>.
type Failure ¶
type Failure struct {
Plugin string `json:"plugin"`
Surface string `json:"surface"`
What string `json:"what"`
Error string `json:"error"`
At time.Time `json:"at"`
}
Failure is the latest thing a plugin's executable got wrong, kept so the dashboard can show it and offer to report it to the plugin's author.
func Failures ¶
Failures returns the plugin's recent failures, newest first. A failure kept by a lerd from before the log file is the whole history until a new one.
func LastFailure ¶
LastFailure returns the plugin's latest recorded failure.
type Feature ¶
type Feature struct {
Kind string `json:"kind"`
Name string `json:"name"`
Detail string `json:"detail,omitempty"`
}
Feature is one thing a plugin adds, as the dashboard lists it.
type Listing ¶
type Listing struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Repo string `json:"repo"`
Commit string `json:"commit"`
Author string `json:"author,omitempty"`
Category string `json:"category,omitempty"`
Tags []string `json:"tags,omitempty"`
// Version is the approved commit's own version, as its plugin.yaml says.
Version string `json:"version,omitempty"`
// Lerd is the approved commit's own `lerd` constraint.
Lerd string `json:"lerd,omitempty"`
// Requires is what the approved commit needs on the machine, as
// Plugin.Requirements reads it.
Requires []string `json:"requires,omitempty"`
// Platforms is the approved commit's own `platforms`, empty for both.
Platforms []string `json:"platforms,omitempty"`
// Images are copied by the registry at the approved commit and published
// beside the index, the first one being the preview. The index lists them
// relative to itself; LoadCatalog hands them out as absolute URLs.
Images []string `json:"images,omitempty"`
// AddedAt and UpdatedAt are the days the plugin was first approved and its
// current commit was; Stars is its repository's count when the index was built.
AddedAt string `json:"added_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Stars int `json:"stars,omitempty"`
}
Listing is one approved plugin: its repository and the exact commit a maintainer approved, which is the only one lerd will install.
func CachedListing ¶
CachedListing is the catalog's listing for name from the copy lerd already holds, never fetching: for showing what the catalog says about a plugin that is installed, where a slow network must not hold the list up.
func CachedUpdate ¶
CachedUpdate is PendingUpdate read from the catalog lerd already holds, never fetching: the dashboard draws updates without waiting on the network, and the hourly refresh brings new ones in.
func FindListing ¶
FindListing returns the approved listing called name.
func PendingUpdate ¶
PendingUpdate returns the newer approved listing for an installed catalog plugin, or false when it is current, unlisted, needs a newer lerd or the catalog is unreachable.
func (Listing) ChangesURL ¶
ChangesURL is GitHub's comparison of the installed commit with the approved one, "" for a repository GitHub does not host.
type MCPTool ¶
type MCPTool struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
InputSchema map[string]any `yaml:"input_schema,omitempty" json:"input_schema,omitempty"`
Run string `yaml:"run" json:"run"`
// Timeout raises MCPLimit for this tool, up to ten minutes.
Timeout string `yaml:"timeout,omitempty" json:"timeout,omitempty"`
}
type Manifest ¶
type Manifest struct {
APIVersion int `yaml:"api_version" json:"api_version"`
Name string `yaml:"name" json:"name"`
Version string `yaml:"version,omitempty" json:"version,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
// Lerd is the oldest lerd release the plugin needs, as a constraint like ">=1.37".
Lerd string `yaml:"lerd,omitempty" json:"lerd,omitempty"`
Commands []Command `yaml:"commands,omitempty" json:"commands,omitempty"`
SiteCommands []SiteCommand `yaml:"site_commands,omitempty" json:"site_commands,omitempty"`
Hooks map[string]string `yaml:"hooks,omitempty" json:"hooks,omitempty"`
MCP *MCP `yaml:"mcp,omitempty" json:"mcp,omitempty"`
API []Endpoint `yaml:"api,omitempty" json:"api,omitempty"`
UI *UI `yaml:"ui,omitempty" json:"ui,omitempty"`
// Provides maps each job the plugin takes over from lerd to the executable
// lerd runs for it.
Provides map[string]string `yaml:"provides,omitempty" json:"provides,omitempty"`
// Requires names the commands the plugin's scripts call beyond their own
// interpreters, which lerd reads from each script's first line.
Requires []string `yaml:"requires,omitempty" json:"requires,omitempty"`
// Platforms lists where the plugin runs, linux or macos; empty means both.
Platforms []string `yaml:"platforms,omitempty" json:"platforms,omitempty"`
// Icon is a single-colour SVG drawn for the plugin on its card, whatever it
// adds; a rail page's own icon stands in when it is empty.
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"`
// Docs is a markdown file of instructions the dashboard shows once the
// plugin is installed, apart from the README its repository shows.
Docs string `yaml:"docs,omitempty" json:"docs,omitempty"`
// Screenshots are images in the plugin shown on its card in the dashboard.
Screenshots []string `yaml:"screenshots,omitempty" json:"screenshots,omitempty"`
// Notifications says the plugin may tell the user something through lerd's
// notifications, which it can only do once the user has allowed it.
Notifications bool `yaml:"notifications,omitempty" json:"notifications,omitempty"`
}
type Plugin ¶
Plugin is a validated manifest and the directory it was read from.
func Install ¶
Install copies or clones src into a staging dir inside the plugins dir, validates it, then moves it under the name its manifest declares. Nothing is left behind when the plugin turns out to be invalid.
func InstallListed ¶
InstallListed installs an approved plugin at the commit it was approved at.
func ProviderOf ¶
ProviderOf returns the installed plugin in charge of job, or nil when lerd does the job itself.
func ReadPlugin ¶
ReadPlugin reads and validates the plugin in dir. Unknown keys are ignored so a manifest written for a newer lerd still loads, but an unknown value in a known key (an event, a method) is refused by name.
func (*Plugin) CardIcon ¶
CardIcon is the icon the dashboard draws for the plugin: its own, or its rail page's, "" when it has neither.
func (*Plugin) Command ¶
func (p *Plugin) Command(run, surface string, args []string, extra map[string]string) (*exec.Cmd, error)
Command builds the process for one of the plugin's files, with its context as LERD_* variables plus what the surface knows in extra. It creates the data dir first, so a script can write there without checking.
func (*Plugin) DocsHTML ¶
DocsHTML renders the docs file offline, "" when there is none. Raw HTML is dropped, local images resolve through fileURL, and only web links stay links: a relative one points into a repository the reader may not have.
func (*Plugin) Features ¶
Features lists what the plugin adds, named the way the user meets each one.
func (*Plugin) Provide ¶
Provide runs the plugin's executable for job as `<run> <action>`, with the request as JSON on stdin, and returns what it printed. A failure carries what the plugin wrote to stderr, since that is the only reason anyone gets.
func (*Plugin) Requirements ¶
Requirements lists what the plugin needs on this machine: the interpreter each of its executables names on its first line, and the commands its manifest declares under requires:.
type Revocation ¶
type Revocation struct {
Name string `json:"name"`
Reason string `json:"reason"`
At string `json:"at"`
Commits []string `json:"commits,omitempty"`
}
Revocation is the catalog pulling a plugin for a security problem, with the reason every user who has it is shown. Commits are the approved commits it covers; with none it covers every install of the name.
func Revoked ¶
func Revoked() []Revocation
Revoked lists the installed plugins a revocation is stopping now.
type RevokedError ¶
type RevokedError struct{ Revocation }
RevokedError is why a revoked plugin did not load.
func (RevokedError) Error ¶
func (e RevokedError) Error() string
type SiteCommand ¶
type SiteCommand struct {
Name string `yaml:"name" json:"name"`
Label string `yaml:"label" json:"label"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Confirm bool `yaml:"confirm,omitempty" json:"confirm,omitempty"`
Run string `yaml:"run" json:"run"`
// Timeout raises SiteCommandLimit for this command, up to an hour.
Timeout string `yaml:"timeout,omitempty" json:"timeout,omitempty"`
}
SiteCommand runs against one site, from its Commands menu and the palette.
func (SiteCommand) Limit ¶
func (c SiteCommand) Limit() time.Duration
type Source ¶
Source records which listing and commit an installed plugin came from, kept in the plugin's folder so it goes with the install.
func InstalledSource ¶
InstalledSource reports the listing commit an installed plugin came from; false for one installed from a folder or an unlisted repository.
type UI ¶
type UI struct {
Page *UIPage `yaml:"page,omitempty" json:"page,omitempty"`
SiteTab *UIPage `yaml:"site_tab,omitempty" json:"site_tab,omitempty"`
// OverviewCard is drawn on the Overview of the sites the user switched the
// plugin's card on for.
OverviewCard *UIPage `yaml:"overview_card,omitempty" json:"overview_card,omitempty"`
// PageScript is injected into the pages of the sites the user switched the
// plugin on for, and runs in those sites' own origin.
PageScript string `yaml:"page_script,omitempty" json:"page_script,omitempty"`
}