Skip to content
GitHubRSS

src/orchestrator/plugin.ts — the VxPlugin interface + installer

The integration seam. A plugin is definePlugin(import.meta, hooks) — its name is the name of the package it is defined in, read from the nearest package.json and stamped where the workspace loader checks, never a field — with at least one capability; defineWorkspace({ plugins: [...] }) activates it. Core consults capabilities at fixed points and otherwise ignores plugins — behavior lives in the plugin package (vite-style), not in core.

CapabilityConsulted byContract
executor(ctx)plugin-host.tsreturn a TaskExecutor or decline; ALL kept in order, first accepting runs
config(ws, ctx)every verb, firstedit the workspace config in place before anything is derived from it (cacheDir too); re-validated after EACH plugin
project(cfg, ctx)per loaded configadd/remove/edit a project’s tasks in place; core re-validates after EACH plugin, by name
graph(nodes, ctx)after graph buildedit deps/requested in place; the builder’s checks run again (item 981)
key(task, ctx)per task, at hash{ name: value } material folded into the key and named in vx why
fingerprintclaim, static{ files, affected(change, ctx) }: the workspace-fingerprint files this plugin keys per project; one claimant each
schedule(nodes, ctx)before schedulingtask id → weight, merged over the structural baseline; later plugin wins per task
admit(task, ctx)every local dispatchfalse holds a ready task beside ctx.running until something finishes (with nothing running, it is overridden, by name); sync, cheap; a throw or a Promise admits from then on
commandsunknown CLI verb{ verb: { description, run(argv, ctx) } }; a core verb’s name is refused; vx help
cache(ctx)run setupreturn a CacheLayer or decline; ALL kept in order and chained (see chained-cache.md)
telemetry(ctx)telemetry-host.tsreturn sink(s) or decline
setup(ctx)installPluginsvalidate config; throw UserError
teardown()end-of-runflush/close; crash-isolated, 3s-bounded
  • Decline-fast: every capability must return undefined cheaply when unconfigured — a plain run with declared-but-unconfigured plugins is zero-overhead (measured ~116ms unchanged).
  • A throw in setup, a stage (config, project, graph, key, schedule) or an executor / cache factory fails the run in one line naming the plugin and the hook: what a plugin shapes is load-bearing. The observers are isolated (observability never breaks a run): a telemetry factory or sink, a ctx.on handler and teardown are warned and switched off, and a throwing admit (or one that answers a Promise) admits from then on. An async hook’s rejection counts as its throw. ctx.on with a hook name it does not know fails the load (H-16).
  • teardown() and every telemetry sink’s flush() ARE invoked at end-of-run, each under try/catch and a time bound — plugins may rely on them to drain buffers. A run a SIGINT/SIGTERM/SIGHUP stops is no exception (item 849), nor is one that never started its schedule (a refused setup, a factory that threw, an unresolved name) or a plan (item 1021); a second signal, or a kill -9, is. A plugin whose own setup threw is not torn down. (The older eventSink seam is gone since pipeline v2; setup(ctx) on the bus and telemetry are the two observe paths.)
  • No defaults, one floor. Core applies no plugin on its own; its local executor and local cache are appended at the tail of every list and chain (see plugin-host.md), so a workspace that declares nothing runs and caches here.