* plugins: install effect plug-in bundles (wasm + ScriptUI panels) in one go * plugins: address review - keep listing errors, refuse replacing an installed plug-in * plugins: report unreadable bundle entries when their metadata cannot be read * plugins: surface partial install errors in the menu bar, cover the install flow with tests * plugins: surface partial install errors for async dialog answers too * plugins: show result errors as compact status text * plugins: harden installs - confined automation, no panel overwrite, size cap, cleanup errors * plugins: bound install reads from one handle, detect unreadable panels, clarify install docs * plugins: drop an unneeded mut in the bounded read * plugins: saturate the bounded read limit * plugins: install into the session's Plug-ins folder, refuse hidden single installs and case-variant panels - Install .wasm files into Session::plugins_dir() (the folder the Plug-in Library from #2524 uses), not a path rebuilt from the settings folder; the host tests now point both at their temp folder so they never touch the real settings. - A single install of a hidden file (".x.wasm") was skipped and reported as a success that installed nothing: it is now an error. - Installed names must be bare file names (and not Windows device names on Windows), as file.installScript requires, so every installed panel is listed and can be uninstalled. - A panel whose name differs from an installed one only in case replaced it on Windows and macOS: compare ignoring case and check the file on disk too. - The loaded plug-in's source is the installed copy, not the file it came from. - Document the `bundle` menu parameter, so checked execution accepts it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * plugins: drop needless to_vec in the bundle install test (clippy) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * plugins: keep the loader test's byte vectors, drop to_vec in the install test (clippy) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: STAR Web Studio <hello.starwebstudio@gmail.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
28 KiB
Plug-ins and extensions
EffectCraft's core aims at After Effects parity. What After Effects users get from third parties (effect plug-ins, scripts, ScriptUI panels) is an extension in EffectCraft too, and EffectCraft offers the same kinds of extension points as After Effects so that anyone can write their own.
Extension points compared with After Effects
From After Effects' public documentation (the user guide's scripting pages and the Scripting Guide):
| After Effects | EffectCraft | |
|---|---|---|
Effect plug-ins (C/C++ SDK, .aex / .plugin) |
Effect plug-ins: WebAssembly modules (plug-in API v1) or Rust; the Plug-ins folder next to the settings loads at start-up |
Different ABI: After Effects SDK plug-ins can't run (G8) |
| Scripts folder: its scripts are listed in File ▸ Scripts | Scripts/ in the settings folder (File ▸ Scripts ▸ Install Script File… copies a script there), listed in File ▸ Scripts with the bundled samples; a subfolder (one level) is a submenu named after it |
Same |
File ▸ Scripts ▸ Run Script File… (.jsx, .jsxbin) |
.jsx / .js |
Gap: .jsxbin (an undocumented binary encoding) |
| File ▸ Scripts ▸ Recent Script Files: the 10 most recently run scripts; Cmd+Option+Shift+D (Ctrl+Alt+Shift+D on Windows) reruns one | File ▸ Scripts ▸ Recent Script Files lists the 10 scripts run last (script files and File ▸ Scripts entries, newest first, kept in the settings); Cmd+Alt+Shift+D runs the most recent one again (file.runScript {"recent": 0}, a key that can be changed in Edit ▸ Keyboard Shortcuts) |
Same |
ExtendScript preprocessor directives (#include, #target, #targetengine) |
#include / //@include / #includepath inline files relative to the script (once each, depth-limited, file gate applies); #target, #targetengine, #strict are ignored |
Same |
Scripts/Startup and Scripts/Shutdown: run in alphabetical order when the app starts and quits |
Scripts/Startup/ and Scripts/Shutdown/ in the settings folder, in alphabetical order (case ignored): the Startup scripts run at launch before a project opens, and the Shutdown scripts run as the desktop app quits, after the project is closed. A Startup script that fails is reported in a toast and in the log, a Shutdown script that fails is reported in the log, and the next script still runs |
Partial: each script runs in its own global scope, so variables and functions a Startup script defines are not available to later scripts; a Startup script's modal dialog stays open while the next scripts run and the app starts; a Shutdown script's modal dialogs close as if canceled; the web app runs no Shutdown scripts |
Scripts/ScriptUI Panels: listed at the bottom of the Window menu, dockable like the app's own panels, this is the panel |
Scripts/ScriptUI Panels/ in the settings folder (File ▸ Scripts ▸ Install ScriptUI Panel… copies one there), listed at the bottom of the Window menu (a subfolder, one level, is a submenu named after it), docked like any panel, this is the panel; the panels open when the app quits open again at the next launch |
Same. Where a panel was docked isn't kept: workspace layouts aren't saved between launches |
| Keyboard shortcuts for scripts (the Keyboard Shortcuts editor lists the scripts of the Scripts folder) | Edit ▸ Keyboard Shortcuts lists the scripts of File ▸ Scripts under File ▸ Scripts (file.runScript {"name": "<file name>"}); a key given to one is saved with the preset, File ▸ Scripts shows it, and pressing it runs the script as its menu entry does |
Same. After Effects binds a key to a script's place in the alphabetical list (ExecuteScriptMenuItem01…), and EffectCraft binds it to the script's file name. The Window menu entries of ScriptUI panels aren't offered |
Running a script from the command line (afterfx -r script.jsx) |
effectcraft-cli script, the control channel's and MCP's script.run |
Same purpose, other entry points |
app.settings (kept in the preferences between sessions) |
Kept in script_settings.json in the settings folder (script.settings.get / script.settings.save) |
Same |
| Allow Scripts to Write Files and Access Network | Settings ▸ Scripting & Expressions | Same |
The settings folder is ~/Library/Application Support/EffectCraft on macOS, %APPDATA%\EffectCraft
on Windows and $XDG_CONFIG_HOME/effectcraft (or ~/.config/effectcraft) on Linux
(preferences.md); the web app keeps it in browser storage.
Scripts
Scripts use After Effects' scripting object model (app, app.project, CompItem, layers,
Property with keyframes and eases, app.beginUndoGroup…), so most scripts written for After
Effects' documented API run unchanged. Every edit a script makes is an engine command: undoable,
journaled and the same as the UI's action. The effectcraft-script crate's docs
(crates/script/src/lib.rs) list the object model.
- Run one: File ▸ Scripts ▸ Run Script File…, the Script Console panel,
effectcraft-cli script file.jsx, orscript.run {code, name}over the control channel and MCP. - Install one: File ▸ Scripts ▸ Install Script File… (or copy it into
Scripts/in the settings folder). It is listed in File ▸ Scripts at once (After Effects asks for a restart). - Run one at every launch or quit: copy it into
Scripts/Startup/orScripts/Shutdown/in the settings folder. These scripts are not listed in File ▸ Scripts.file.runStartupScriptsandfile.runShutdownScriptsrun them and return{ran, waiting, failed}; the desktop app calls them at launch and quit, and the web app callsfile.runStartupScriptsat launch. file.scripts.listlists installed and bundled scripts and panels;file.runScript {name}runs one;file.uninstallScript {name}removes an installed one.- A subfolder of
Scripts/is a submenu of File ▸ Scripts named after the folder, and its scripts are namedFolder/File.jsxinfile.scripts.listandfile.runScript. One level of folders is listed; deeper folders are not.file.uninstallScriptremoves only scripts at the top of the Scripts folders: delete a script in a subfolder yourself. A subfolder may be a link to another folder (an extension's own), and uninstall never deletes a file through it.
ScriptUI panels
A ScriptUI panel is a script that builds its user interface into the panel it is given as this.
Put it in Scripts/ScriptUI Panels/ in the settings folder (or use File ▸ Scripts ▸ Install
ScriptUI Panel…) and it is listed at the bottom of the Window menu under its file name; choosing it
runs the script and docks its panel as a tab, which you can move, float, maximize and close like any
other panel. Closing the tab closes the panel (its onClose runs). Panels open when EffectCraft
quits open again at the next launch (window.restoreScriptPanels, which the desktop and web apps
run at start-up; the list is scriptui_panels.json in the settings folder).
To group panels, for example the several panels of one extension, make a subfolder of
Scripts/ScriptUI Panels/ and put them in it: Scripts/ScriptUI Panels/My Tools/Animate.jsx is
listed as Window ▸ My Tools ▸ Animate.jsx, its tab is titled Animate, and commands name it
My Tools/Animate.jsx (window.scriptPanel {name}). One level of folders is listed. Install
ScriptUI Panel… copies a panel to the top of the folder; make the subfolder and move panels into it
yourself, and delete them from it yourself too.
Run from File ▸ Scripts instead, the same script gets no panel, so it should make a floating palette itself. A minimal panel:
// Nudge.jsx: moves the selected layers 10 px right, as a dockable panel.
(function (thisObj) {
var ui = thisObj instanceof Panel ? thisObj : new Window("palette", "Nudge", undefined, { resizeable: true });
ui.orientation = "column";
ui.alignChildren = ["fill", "top"];
var go = ui.add("button", undefined, "Nudge Right", { name: "nudge" });
var status = ui.add("statictext", undefined, "Select layers, then click.");
go.onClick = function () {
var comp = app.project.activeItem;
if (!(comp instanceof CompItem)) return;
var layers = comp.selectedLayers;
app.beginUndoGroup("Nudge Right");
for (var i = 0; i < layers.length; i++) {
var p = layers[i].transform.position;
var v = p.value;
if (p.numKeys === 0) p.setValue([v[0] + 10, v[1]]);
}
app.endUndoGroup();
status.text = "Moved " + layers.length + " layer(s).";
};
if (ui instanceof Window) ui.show(); else ui.layout.layout(true);
})(this);
Supported controls: panel, group, button, iconbutton, statictext, edittext,
checkbox, radiobutton, slider, scrollbar, progressbar, dropdownlist, listbox,
tabbedpanel / tab and image, with ScriptUI's automatic layout (orientation,
alignChildren, alignment, margins, spacing, preferredSize), resource strings, event
handlers (onClick, onChange, onChanging, onClose, addEventListener) and onDraw with
ScriptUIGraphics. Mouse events (addEventListener("mousedown" | "mouseup" | "mousemove" | "mouseover" | "mouseout" | "click", handler, capture?)) reach the control under the pointer and
its containers (capture, target, bubble; stopPropagation()), with a MouseEvent carrying
clientX/clientY relative to the target, button, detail (click count) and the modifier
keys. Agents use a panel like a user: scriptui.list, scriptui.get (its controls),
scriptui.click, scriptui.set, scriptui.mouse and scriptui.close; every control has the automation id
scriptui.<window>.<control id or name>. The bundled sample Layer Tools.jsx
(crates/engine/scripts/ScriptUI Panels/) and the Ease Presets extension below are longer
examples.
Bundled extensions
Optional tools that ship with EffectCraft but are not part of its core: they are scripts on the
public scripting API, like the third-party scripts After Effects users install, and are listed
with the bundled samples (file.scripts.list reports them as extension). A script of the same
name in your Scripts folders takes their place.
Ease Presets (extensions/scriptui-panels/Ease Presets.jsx)
Window ▸ Ease Presets.jsx: easing curves kept by name and applied to the sides of selected keyframes (#254; it was a core panel in v0.6.0). A curve is the out side of a segment's first key and the in side of its second, in the Keyframe Velocity dialog's terms: influence in percent, and speed relative to the segment's average speed (1 = as fast as a straight line, 0 = at rest), so one curve fits any duration and change of value.
- Click a preset to apply it to the selected keyframes of each selected property, in one undo step. Ease chooses the sides (#1329): In eases each key's incoming side with the curve's in side, Out its outgoing side with the curve's out side, In-Out (the default) both. A side is eased when it has a neighbouring keyframe, selected or not (the neighbour doesn't change), with its speeds scaled to the segment on that side: per dimension, and along the motion path for spatial properties. So a single keyframe works, a selected pair gets the whole curve, and the first and last keys ease the side that has a segment. The side not eased keeps playing as it did (a Hold stays a Hold). Roving keyframes are skipped, and the status says how many: they pass speed straight through, so an ease on one wouldn't play (turn off Roving to ease it). The keys stay selected, so you can try another.
- The drawing shows the working curve; type its numbers (Out % and Speed, In % and Speed) and click Apply, or click From Keys to read the curve between the first selected pair of neighbouring keys.
- Save keeps the working curve under the typed name (replacing a preset of that name); Rename and Delete act on the selected preset of your own. The twelve built-in presets can't change.
- User presets are kept with
app.settings(sectionEase Presets, keyuserPresets, inscript_settings.jsonin the settings folder). Presets saved by the v0.6.0 core panel (ease_presets.json) move there the first time a later version loads its settings; the old file is left as it was.
It uses only After Effects' documented scripting API (comp.selectedProperties,
Property.selectedKeys, keyInTemporalEase / keyOutTemporalEase, setTemporalEaseAtKey with
KeyframeEase, setInterpolationTypeAtKey, app.settings, ScriptUI), so it is also a worked
example of a keyframe tool.
Effect plug-ins
EffectCraft loads third-party effects through a small, versioned plug-in API (API version 2; WebAssembly ABI 1).
A plug-in effect behaves exactly like a built-in one: it is listed in Effects & Presets (and the
Effect menu) under its category, applied by id (effect.apply {"effect": "org.example.posterize"})
or display name, its parameters are ordinary properties (keyframes, expressions, Effect Controls,
prop.set), and it is saved in projects by id.
There are two ways to write one:
| WebAssembly plug-in | Rust plug-in | |
|---|---|---|
| What | a .wasm (or .wat) module |
a type implementing effectcraft_effects::plugin::EffectPlugin |
| Loaded by | Effect ▸ Install Effect Plug-in… (copied into the Plug-ins folder next to the settings and loaded on every start-up), Effect ▸ Load Effect Plug-in… / effect.plugins.load {path | folder} (loads in this session only), the Plug-ins folder next to the settings at start-up, or effect.plugins.install {path | folder} |
register_plugin(Arc::new(MyEffect)) in an app that links EffectCraft's crates |
| Sandbox | yes: no imports (no files, clock or network), fuel-limited, at most 1 GiB of memory, deterministic floats | none (it is your code) |
| Platforms | desktop and CLI (the wasmi interpreter, crate feature effectcraft-plugin/wasm); not the web build |
everywhere |
effect.plugins.list lists what is loaded: {api, wasm, plugins: [{id, name, category, version, author, source, params}]}.
The contract
- Pixels are premultiplied RGBA
f32(0–1; values may exceed 1 in 32 bpc projects), row major, processed in place. The buffer is the layer's (after masks and the effects above), possibly at preview resolution (scale= buffer pixels per layer pixel) and padded. - Parameters arrive flattened to
f64s in the order they are declared: slider, angle, checkbox (0 / 1) and popup (0-based index) take one value, point two (already in buffer pixels), colour four (RGBA 0–1). - Time is layer time in seconds.
- Rendering must be deterministic and may run on several threads at once (a WebAssembly plug-in gets one instance per thread).
- A failed render (an error code, a trap, running out of fuel) leaves the frame unchanged.
The manifest
Every plug-in describes itself with a JSON manifest:
{
"api": 1,
"id": "org.effectcraft.example.posterize-bands",
"name": "Posterize Bands",
"category": "Stylize",
"version": "1.0.0",
"author": "EffectCraft contributors",
"description": "Quantizes luminance into tinted bands.",
"params": [
{"id": "levels", "name": "Levels", "type": "slider", "default": 4, "min": 2, "max": 64, "sliderMax": 16, "decimals": 0},
{"id": "mix", "name": "Tint Amount", "type": "slider", "default": 50, "min": 0, "max": 100},
{"id": "tint", "name": "Tint", "type": "color", "default": [1.0, 0.55, 0.2, 1.0]},
{"id": "invert", "name": "Invert Bands", "type": "checkbox", "default": false}
]
}
id: stable, reverse-domain; letters, digits,.,_,-.ec.is reserved for built-ins. Ids must be unique (a second plug-in with a taken id is refused).category: one of the Effects & Presets categories (Blur & Sharpen,Stylize, …) or a new one, which appears after the built-in categories.- Parameter types:
slider(default,min,max, optionalsliderMin,sliderMax,decimals),angle(defaultdegrees),checkbox(defaultbool),popup(options,defaultindex),point(defaultas fractions of the layer size,[0.5, 0.5]= centre),color(defaultRGBA 0–1). Parameter ids are letters, digits and_, unique; at most 256 parameters; a slider needsmin ≤ default ≤ max(andsliderMin ≤ sliderMax); the manifest is at most 1 MiB.
WebAssembly ABI (v1)
A module with no imports that exports:
| export | signature | |
|---|---|---|
memory |
memory | linear memory shared with the host |
ec_api_version |
() -> i32 |
must return 1 |
ec_manifest_ptr, ec_manifest_len |
() -> i32 |
the UTF-8 JSON manifest in memory |
ec_alloc |
(bytes: i32) -> i32 |
a buffer of at least bytes bytes, 8-byte aligned; it may reuse the previous one |
ec_render |
(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, time: f64, scale: f64) -> i32 |
process the pixels in place; return 0 on success |
For each frame the host calls ec_alloc once for a buffer holding the parameters (nparams
little-endian f64s) followed by the pixels (width × height × 4 little-endian f32s), fills it,
calls ec_render, and reads the pixels back. Each call gets a fuel budget proportional to the
frame size; a module that loops forever stops with an error instead of hanging the app.
Example
examples/plugins/posterize-bands is a complete plug-in in
plain Rust (no dependencies):
cd examples/plugins/posterize-bands
cargo build --release --target wasm32-unknown-unknown
# then, in EffectCraft: Effect ▸ Install Effect Plug-in…
# → target/wasm32-unknown-unknown/release/posterize_bands.wasm (copied into Plug-ins, loads on start-up)
# or in-session only: Effect ▸ Load Effect Plug-in…
# or headless:
effectcraft-cli exec effect.plugins.load '{"path": "target/wasm32-unknown-unknown/release/posterize_bands.wasm"}'
Bundles. effect.plugins.install {"folder": ...} installs a whole plug-in folder in one
go: every .wasm at its top level is copied into the Plug-ins folder (and loads at once),
and every .jsx at its top level is installed as a ScriptUI panel — so a third-party plug-in
ships as one folder the user installs once. A .wasm that does not load is not copied (the
install rolls it back), and per-file problems in a folder install come back in errors
instead of failing the whole bundle.
What an install does and does not do. It is a security boundary: it copies code into a folder the app runs at every start, so:
- Only
.wasm(path/ a bundle's.wasm) and.jsx(a bundle's panel) are installed; other files in a bundle (readmes, manifests, sources) are skipped. Files are named by their own entry name, so a bundle cannot carry a path, a..component or a name that escapes the destination folder. - Nothing is overwritten. An existing plug-in or panel with the same file name makes that file
fail with "already installed": delete it from
Plug-ins/Scripts/ScriptUI Panelsfirst to update it (versioning the file name is the other option). A bundle is installed file by file, so one collision does not undo the rest. - Files over 64 MiB are refused before they are read, rather than copied into a startup folder.
- A
.wasmis loaded once, in the same sandbox it runs in later (see below), so a module that cannot load is reported instead of failing every start. That load runs the module's start-up code and its metadata exports;ec_renderis never called, and a panel's script is not run at install time (it runs when the panel opens, as with any installed panel). The copy made before the load is then removed again — best effort: if removing it fails, the command says so and names the file, and you delete it fromPlug-insyourself. - A
.wasmis uninstalled by deleting it fromPlug-ins(the next start no longer loads it): there is noeffect.plugins.uninstall. A panel installed here is an installed script, sofile.uninstallScript {name}removes it. Replacing either means removing it first. - Hidden files (names starting with
., such as macOS._x.wasmcopies) are skipped in a bundle and refused as a single install. A panel name that differs from an installed panel's only in letter case counts as the same name. - Installed plug-ins share the
Plug-insfolder with the Plug-in Library (which keeps its packages inPlug-ins/Library). An effect id can be registered only once: a plug-in whose id is already loaded, from either kind of install, is refused. At start-up the files installed here load before the library's packages, so if both carry the same id (for example after files were copied by hand), the file here wins and the library package reports the duplicate.
The sandbox. A plug-in runs in the wasmi interpreter with no imports (no file, network or clock access), deterministic floats, a fuel budget per frame and a 1 GiB memory cap, so a plug-in cannot escape the sandbox, hang the app or render differently on another machine. A ScriptUI panel is different: it is a script with the app's scripting object model, so it can read and write project data through commands; file and network access for scripts follow Settings ▸ Scripting & Expressions, exactly as for a script installed with File ▸ Scripts ▸ Install ScriptUI Panel….
Any language that compiles to WebAssembly works (C, Zig, AssemblyScript…) as long as the module
has no imports. crates/plugin/src/tests.rs has a plug-in written directly in WebAssembly text.
Rust plug-ins
use std::sync::Arc;
use effectcraft_effects::plugin::*;
struct Swap(PluginManifest);
impl EffectPlugin for Swap {
fn manifest(&self) -> &PluginManifest { &self.0 }
fn render(&self, frame: &mut PluginFrame, params: &PluginParams, _time: f64) -> Result<(), String> {
if params.b("on") {
for p in frame.pixels.iter_mut() { p.swap(0, 2); }
}
Ok(())
}
}
register_plugin(Arc::new(Swap(manifest)))?; // before the UI builds its menus
PluginParams reads values by parameter id (f, b, point, color, and in API 2 point3,
layer, text). At most 1024 plug-in effects can be registered per process.
Plug-in API 2 (native plug-ins)
API 2 is for Rust plug-ins and for hosts that bring effects from elsewhere (such as
external effect helpers); the WebAssembly ABI stays at 1. Manifests with "api": 1 or 2 are accepted,
effect.plugins.list reports api: 2, and up to 1024 plug-in effects can be registered.
- New parameter types:
point3(default[x, y, z]; x, y arrive in buffer pixels, z as is),layer(another layer of the composition; flattens to its id, -1 for none, read withPluginParams::layer),text(a string field) andhidden(a string that is not shown, for opaque plug-in data).text/hiddentake no slot inflat; their strings are inPluginParams::texts, one per such parameter in declaration order, read withPluginParams::text(id).slidergains"integer": true(shown with 0 decimals) and every parameter an optional"group"path ("Advanced/Edges"; informational). WebAssembly modules can't usepoint3,layer,textorhidden. PluginFramealso carriesorigin(buffer pixels of layer pixel (0, 0); padding shifts it) andlayer_size(layer pixels).EffectPlugin::padding(params, time, scale, layer_size, instance_key) -> u32asks for that many transparent buffer pixels on every side before rendering (glows, regions of definition larger than the input); at most 4096.layer_sizeis the layer's size in layer pixels (so sizes relative to the frame can be measured) andinstance_keyidentifies the applied effect instance (seePluginHost::instance_key).EffectPlugin::render_v2(frame, params, time, host)(default: callsrender) gets aPluginHostwithinput_at(layer_time)(the effect's input at another time, in its own extent; the effects above this one are applied),layer_input(param_id, layer_time)(a layer parameter's layer, effects and masks applied, resampled into the frame's grid),params_at(layer_time),frame_rate(),time(),instance_key()(a stable id of the applied effect instance: the same on every frame, different per layer and per application; plug-ins that keep state between frames pool their native instances by it) andframe_range()(the layer's source time range in layer-time seconds,Nonewhen it has none). Images arePluginImage { width, height, pixels, origin, scale }. It may run on several threads at once.
Compatibility
The API version is checked when a plug-in loads: a manifest or module for another version is refused with a message. Within a version the contract above does not change; additions (new parameter types, new optional exports) bump the version.
Plug-in Library
Settings ▸ Plug-in Library (also Effect ▸ Plug-in Library…) lists WebAssembly effect plug-ins published by the community, and installs, updates and removes them without bundling them into the app. Only WebAssembly effects are listed: they run in the same sandbox as any plug-in, so a package can't read files, open the network or crash the app.
- One reviewed catalog. The list comes from one
index.jsonin thestorytold/effectcraft-pluginsrepository; packages are added by pull request and reviewed there. The app has no field for another catalog;library.refresh {catalog}(for plug-in authors testing an entry) takes anindex.jsonon GitHub or a local file, and the page then says it shows a test catalog until Refresh. - Nothing is downloaded until you ask. Fetch Catalog (
library.refresh) reads the catalog; Install (library.install) downloads the package's.wasm, checks its SHA-256 (and its size, when the catalog gives one) against the catalog and loads it. Downloads use the system'scurl,https://only and starting on GitHub's hosts (redirects stay onhttps://; the SHA-256 check is what guarantees the file). - Where it goes.
<Plug-ins>/Library/<id>/<version>/<id>.wasm(thePlug-insfolder next to the settings), listed inLibrary/installed.json. Installed packages load at start-up, each checked against the SHA-256 it was installed with. The catalog last fetched is kept asLibrary/catalog.json, so the list works offline. - Restart after removing or updating. Registered effects can't be unloaded: removing or updating a package whose effects are loaded takes effect at the next start, and the list says "Restart EffectCraft to finish".
- Preview before installing. Preview (
library.preview) downloads the package's module (checked against the catalog's size and SHA-256) without installing it, and renders its effect in the sandbox on a built-in sample frame with default parameters: 2 seconds at 12 fps, with a Before/After split. The details show it playing, the effects with their parameter names, "Open on GitHub" (the package'srepo) and, when the package has apreview.videolink (anhttps://YouTube, Vimeo or GitHub URL), "Watch video". - Missing effects.
library.missinglists the effects a project uses that aren't installed, and the catalog packages that provide them.
Commands (UI, CLI, control channel and MCP): library.list {query?, tag?, installed?},
library.refresh {catalog?, wait?}, library.preview {id, version?, wait?},
library.install {id, version?, wait?}, library.update {id?, wait?}, library.uninstall {id},
library.missing, library.loadInstalled. Downloads run in the background; wait: true runs
them to the end (scripts and tests).
Publishing a plug-in
publishing-plugins.md walks through it from an empty folder to a listed
package: build, test, release on GitHub, the catalog entry and its fields, testing the entry with a
local file:// catalog, the pull request and its review, updates and removal.