meridian-plugin CLI
meridian-plugin authors, projects, validates, bundles, and publishes a Meridian
plugin from any directory — no platform repository or workspace checkout required.
It is the bin of the @meridian/plugin-cli package
(libs/plugin-cli/package.json); the entry point is
libs/plugin-cli/src/cli.ts.
Task-oriented walkthroughs live elsewhere: this page is the exhaustive command surface. See scaffold a plugin, generate types, validate a workflow, translate a plugin, build and bundle, and publish to the registry.
Synopsis
meridian-plugin <command> [args] [flags]
| Command | Purpose |
|---|---|
new <name> [dir] |
Scaffold a standalone plugin package. |
gen-types [dir=.] |
Project plugin.yaml → generated/{types,nodes}.ts. |
validate <workflow.yaml> [--plugin <dir>]… |
Static-check a workflow against plugins. |
deps [dir=.] |
List a plugin's dependencies: range, version found, origin, state. |
i18n extract [dir=.] --locale <locale> |
Write/complete i18n/<locale>.yaml. |
i18n check [dir=.] |
List missing/orphan i18n keys. |
build [dir=.] |
Bundle the plugin to a self-contained dist/. |
publish [dir=.]… --registry <url> [--dry-run] |
Publish built plugins (dist/) to a registry, dependencies first. |
versions <@scope/name> --registry <url> |
List a plugin's published versions. |
help, -h, --help, (none) |
Print usage. |
Positional directory arguments are the first token that does not start with -.
Unknown flags are ignored except where a value is explicitly consumed
(--plugin, --locale, --registry) and publish --dry-run.
Dispatch and exit codes
cli.ts dispatches on the first argument (main in
libs/plugin-cli/src/cli.ts). The process exit code is the command's return
value.
| Situation | stdout/stderr | Exit |
|---|---|---|
No command, help, -h, --help |
usage text on stdout | 0 |
| Unknown command | Unknown command: <cmd> + usage on stderr |
1 |
i18n with no/unknown subcommand |
thrown usage error (see below) | 1 |
| Any command throws | ✗ <message> on stderr |
1 |
publish refuses a plugin |
problems, then ✗ <n> plugin(s) refusé(s) … on stderr |
2 |
| Command succeeds | command-specific output | 0 |
Uncaught exceptions are funnelled through a single try/catch around main
that prints ✗ <error message> to stderr and exits 1. validate, deps and
i18n check return their exit code directly (0/1) rather than throwing; publish
returns 2 when its checks refuse a plugin.
new
meridian-plugin new <name> [dir]
Scaffolds a standalone plugin package that compiles out of the box. Source:
libs/plugin-cli/src/commands/scaffold.ts.
| Argument | Required | Default |
|---|---|---|
<name> |
yes | — (npm package name, e.g. @acme/example) |
[dir] |
no | <name> with the @scope/ prefix stripped, resolved to an absolute path |
Behavior:
- Errors (
<dir> already contains a package.json) ifdir/package.jsonexists. - Creates
src/behaviors/andworkflows/directories. - Writes the files below, then prints
Scaffolded <name> in <dir>and a next-step line.
| File | Contents |
|---|---|
package.json |
name, version: 0.1.0, type: module; scripts build/gen-types/validate → meridian-plugin …; dep @meridian/shared and devDep @meridian/plugin-cli at ^<version>, the version of the CLI that runs new (the @meridian/* libraries are released under one version); devDep typescript. |
tsconfig.json |
NodeNext, strict, declaration, outDir: dist, rootDir: src. |
plugin.yaml |
apiVersion: meridian/plugin-v1 manifest (sdk: "^0.1.1") with one value set, one type, one context kind, one declared event, a declarative trigger node (event:, no behavior) and one compute node. |
src/behaviors/example.ts |
One NodeBehaviorFactory export (exampleScore). |
workflows/example.workflow.yaml |
apiVersion: meridian/v3 workflow wiring the two nodes. |
.gitignore |
node_modules/, dist/. |
README.md |
Install/gen-types/validate/build instructions. |
The scaffolded manifest and workflow conform to the plugin manifest and workflow DSL schemas.
gen-types
meridian-plugin gen-types [dir=.]
Projects a plugin's plugin.yaml into TypeScript. Source:
libs/plugin-cli/src/commands/gen-types.ts; codegen core is
libs/plugin-cli/src/codegen.ts.
| Argument | Required | Default |
|---|---|---|
[dir] |
no | . |
Behavior:
Loads the plugin at
dirplus its transitivedependsOnclosure via standard Node resolution (require.resolve("<dep>/plugin.yaml")from the plugin folder), then the sibling directories (loadPluginClosureinlibs/plugin-cli/src/discover.ts). AdependsOnpackage found by neither is skipped;validate,buildanddepsreport it as absent.Emits, into
dir/generated/:File Emitted when Contents types.tsthe plugin contributes ≥1 type one export interfaceper contributed type;import typelines for types owned by dependencies (<dep>/types).nodes.ts≥1 node has a behaviorPluginNodeIO(per-nodeinputs/outputsshapes) and theBehaviorFor<Id>alias. A declarative trigger (event:withoutbehavior) has no entry.Declared events (
contributes.events) generate no TypeScript: a coded trigger'sfromEventreceives the event asRecord<string, unknown>.If neither file would be produced, prints
<name>: nothing to generate (no contributed types, no behavior nodes).and writes nothing.On success, prints
<name> → generated/<files>(comma-separated).
Type mapping (mapRef in codegen.ts): primitive → its TS scalar
(String/Code/Uri/Date/DateTime/Time/Duration → string;
Number/Decimal/Integer → number; Boolean → boolean); quantity →
{ value: number; unit: string; system?: string }; ref → { id: string };
any → unknown; list → T[]; coded → a union of literal codes when the
value set is local, else string; object → the referenced interface name.
A referenced object type absent from the dependsOn closure throws
[<plugin>] type "<name>" not found in the dependsOn closure …. See the
TypeRef reference and
codegen internals.
validate
meridian-plugin validate <workflow.yaml> [--plugin <dir>]…
Statically checks a workflow against the SDK primitives plus the given plugins
and their dependsOn closures. No engine or infrastructure is started. Source:
libs/plugin-cli/src/commands/validate.ts.
| Argument / flag | Required | Default |
|---|---|---|
<workflow.yaml> |
yes | — (first non-flag token) |
--plugin <dir> (repeatable) |
no | ["."] |
Behavior:
Missing workflow argument throws
usage: meridian-plugin validate <workflow.yaml> [--plugin <dir>]….Loads each
--plugindirectory and its closure (deduped by name), sets aside a plugin whosesdkrange does not accept the host's plugin contract (reported with kindcontract, left out of the closures and of the pinned addresses, as the host does), reports for each other manifest, in the host's order, thesdkfloor of its declared events and each key that the schema does not know, at any depth, with its path (kindkey,manifestKeyIssues), then checks eachdependsOnrange against the version of the dependency found (kinddependency,dependencyIssues, as the host does: a plugin that cites itself, or whose range is invalid or does not accept that version, is set aside like a plugin refused for its contract, the version found counting whether that dependency is refused itself or not; a dependency found neither innode_modulesnor in a sibling directory, or refused with a version in the range, is reported and its dependent is registered; the problems come in dependency order, a dependency's before those of the plugins that cite it), registers each other manifest's metadata only — value sets, context kinds, dimensions, types, declared events, node metas (projected bycontributedNodeMeta, as in the host); no behavior modules are imported — in dependency order (dependency before dependent). Two plugins that contribute the same node id stop the command withType de nœud « <id> » déjà enregistré (collision de plugin).(exit1).Runs the load checks the host runs, with the same functions (
libs/shared/src/plugin/manifest-keys.ts,libs/engine-core/src/engine/plugin-events.ts,libs/shared/src/plugin/node-contribution.ts,libs/shared/src/plugin/declared-events.ts): a type already declared (set aside before any other check), declarations, examples and default filters of the events, the pinned addresses a declared type cannot take (those of the loaded plugins and of the core, whatever the order of the--pluginflags; the conflict is reported to both plugins), the projection of each node (a node it refuses, such as a trigger withoutpinnedRoutingand withoutevent, is reported and not registered; adefaultSelectoron a node, and a host transport (http,directory) in areceive.transports, are reported and ignored), bindings to a declared event (themapandrequireof an event binding, therequireof a message binding), and thesdkrange of a manifest that uses keys of a later contract (minimum0.1.1for declared events,0.1.3with an eventdefaultSelector,0.1.4when a node describes its reception,0.1.5with a conditional port rule,0.1.6with connections,0.1.7with a node connection by port,urlSchemesorexposeCredentials,0.1.8with theconnectionof an inbound connection type; one problem, for the highest floor). The host's own node checks (validateNode) are not run. Only the loaded closure is seen: a binding to an event declared outside it, and a declared type equal to the address of a plugin outside it, are checked by the host only. Each problem is printed on stderr as- <plugin> › <name> (<kind>) : <message>; the checks and their messages are listed under Load checks. The problem of an absent dependency is followed by a line that says where the CLI looked and how to make it found:- @acme/vitals › @acme/units (dependency) : dépendance « @acme/units » absente, les contributions qui la citent sont écartées. Charger « @acme/units » avec ce plugin. introuvable dans node_modules depuis /work/vitals et parmi les dossiers voisins : installer « @acme/units », placer son dossier à côté de celui du plugin, ou le désigner à validate par --plugin.A note (kind
filter: an eventdefaultSelectorthat reads a field outside the event's contract) is printed the same way underplugins : <n> note(s) d'événements déclarés :and does not change the exit code.An unknown key (kind
key) of a plugin reached only through thedependsOnclosure, neither given with--pluginnor the current directory, is a note too, printed underdépendances : <n> note(s) de clés inconnues :: the host loads that plugin without the key, and the fix belongs to the dependency's author. The unknown keys of the plugins given, and every other problem of a dependency, keep exit1.Parses the workflow (
WorkflowSpec.parse) and runsvalidateSpec, which checks that ports exist, connection types are compatible, context is satisfied, and required inputs are wired.
| Outcome | Output | Exit |
|---|---|---|
| No errors | ✓ <name>: valid (ports exist, connection types compatible, context, required inputs). on stdout |
0 |
| Errors | ✗ <name>: <n> error(s): then one - <error> per line on stderr |
1 |
| Load problems | ✗ plugins : <n> problème(s) de chargement : then one line per problem on stderr (the workflow is still checked) |
1 |
| Declared-event notes | plugins : <n> note(s) d'événements déclarés : then one line per note on stderr |
unchanged |
| Unknown keys of a dependency | dépendances : <n> note(s) de clés inconnues : then one line per key on stderr |
unchanged |
Agent (agent.*) nodes are not projected by the CLI and are reported as unknown
node types. See validate a workflow and the
static validation engine.
deps
meridian-plugin deps [dir=.]
Lists the dependencies a plugin declares (dependsOn) and their state, read-only.
Source: libs/plugin-cli/src/commands/deps.ts.
| Argument | Required | Default |
|---|---|---|
[dir] |
no | . |
Behavior:
Loads the plugin at
dirand its closure likevalidate(node_modules, then the sibling directories), applies the contract check to the plugins found, and runs thedependsOncheck ofvalidateandbuild(dependencyIssues). The plugin atdirhas its ranges checked whatever its own contract.Prints
<name>@<version> : <n> dépendance(s)(oraucune dépendance), then one line per dependency with its range, the version found, its origin and its state:@acme/vitals@0.2.0 : 2 dépendance(s) @posos/common ^0.4.0 0.4.0 voisin ok @posos/clinical ^0.3.0 0.2.4 node_modules hors plageState DependencyStateMeaning Problem okokThe version found is in the range. no sans contrainteunconstrainedThe range is *(or empty).no hors plageout-of-rangeThe version found is outside the range: the plugin is refused. yes plage invalideinvalid-rangeThe range is not a semver range: the plugin is refused. yes absenteabsentFound neither in node_modulesnor in a sibling directory (version and origin—).yes refuséerefusedFound with a version in the range, but refused for its contract or one of its own entries. yes auto-référenceselfThe entry names the plugin itself: the plugin is refused. yes The second column is the state
dependencyIssuesreturns (@meridian/shared/plugin/dependencies); the CLI prints the first.Prints the plugin's problems under
✗ <name> : <n> problème(s) de dépendance :, one line each asvalidateprints them, the hint line of an absent dependency included.<n>counts the rows marked as a problem above, and nothing else.When the plugin at
diris refused for its own contract, that refusal is printed apart, under✗ <name> : plugin refusé pour son contrat :, and is not counted among its dependency problems; its dependencies are still listed with their states.For each
refuséedependency, prints the problems that refuse it (its contract, or one of its own ranges) underdépendances : <n> note(s) de refus :.Exits
1when the plugin is refused for its contract or has a dependency problem,0otherwise.Rewrites nothing: moving a range to another series asserts a compatibility, which is checked plugin by plugin.
i18n extract
meridian-plugin i18n extract [dir=.] --locale <locale>
Writes or completes the translation overlay i18n/<locale>.yaml. Keys are
derived from the manifest, never invented: labels, descriptions and docs, the hints
of a node's configuration workshop and of a protocol's transports
(nodes.<id>.receive.transports.<name>.hint). Source:
libs/plugin-cli/src/commands/i18n.ts (key schema:
@meridian/shared/plugin/i18n; the list is in
Translate a plugin).
| Argument / flag | Required | Default |
|---|---|---|
[dir] |
no | . |
--locale <locale> |
yes | — |
Behavior:
- Missing
--localethrowsusage: meridian-plugin i18n extract [dir=.] --locale <locale>. - Reads the manifest, derives i18n entries, and merges them with any existing
i18n/<locale>.yaml:- Manifest-derived keys are laid out in manifest order.
- Existing translated values are preserved.
- Newly-derived keys with no value get an empty placeholder preceded by a
# source: <text>comment. - An existing
runtime:section is preserved verbatim at the end.
- Creates
i18n/if needed and writes the file. - Prints
<name> → i18n/<locale>.yaml (<total> clé(s) au total, <added> ajoutée(s)). - Keys present in the bundle but absent from the manifest are reported as orphans
(kept as-is):
⚠ <n> clé(s) orpheline(s) …followed by one- <key>per line.
Keys follow the <section>.<id>.<path> structure. See
translate a plugin.
i18n check
meridian-plugin i18n check [dir=.]
Lists missing and orphan i18n keys across all i18n/*.yaml bundles. Source:
libs/plugin-cli/src/commands/i18n.ts.
| Argument | Required | Default |
|---|---|---|
[dir] |
no | . |
Behavior:
- Reads the manifest and derives the known key set.
- If
i18n/is absent or contains no*.yaml/*.yml, prints a "nothing to check" line and exits0. - For each bundle (sorted by filename):
- The source locale (bundle name equal to
manifest.locale) is exempt from manifest keys; only itsruntimesection matters. Reported✓ <locale>: locale source (runtime uniquement, clés du manifeste non requises). - Other locales: any manifest key without a value is missing; any bundle key outside the manifest is an orphan.
- The source locale (bundle name equal to
- Output per non-clean locale:
<locale>:, then✗ <n> manquante(s) :/⚠ <n> orpheline(s) (hors manifeste) :with one- <key>per line. Clean locales print✓ <locale>: à jour (<n> clé(s)).
| Outcome | Exit |
|---|---|
| No missing keys anywhere (orphans allowed) | 0 |
| At least one missing key | 1 |
build
meridian-plugin build [dir=.]
Builds the deliverable dist/ of a plugin: an esbuild bundle with runtime
dependencies inlined so the artifact is self-contained. Source:
libs/plugin-cli/src/commands/build.ts. There is no build-manifest.mjs; the
CLI does the bundling.
| Argument | Required | Default |
|---|---|---|
[dir] |
no | . |
Behavior:
Loads the plugin's
dependsOnclosure and runs the load checks ofvalidateon it; a problem of the built plugin itself is printed, an unknown key of its manifest, adependsOnrange that does not accept the dependency found, a dependency absent or refused, and an address conflict with a plugin of its closure included, and the build throws<name> : <n> problème(s) de chargement, dist/ non écrit.before anything is written (a published version is immutable). The problem of an absent dependency is followed by the hint line ofvalidate, and the problems that refuse arefuséedependency are printed underdépendances : <n> note(s) de refus :, asdepsprints them. A note is printed under<name> : <n> note(s) d'événements déclarés :and the build goes on.Collects every
module:referenced by the manifest — nodebehavior,receiveandassistmodules, port adapters, event sources, message bindings and port sandboxes (manifestModules) — and resolves each to its source (./x.ts, or./x.jswith source undersrc/;resolveEntry). An unresolvable module throws<root> : module du manifeste introuvable : <module> ….Removes and recreates
dist/.If there are modules, runs esbuild with these settings:
esbuild option Value bundletruesplittingtrue(shared code →chunks/[name]-[hash])formatesmplatformnodetargetnode20external["node:*", "@meridian/*"]— Node builtins and the SDK stay external (provided by the host)banner.jsESM-compat shim defining require,__filename,__dirnamelogLevelwarningoutbaseissrc/when all entries live undersrc/(scaffold layout), else the plugin root. A non-empty esbuild error list throwsesbuild : <n> erreur(s).If a
tsconfig.build.jsonortsconfig.jsonexists, runsnpx tsc -p <tsconfig> --emitDeclarationOnly --noCheckfor the./typesexport consumed by dependent plugins. A non-zero status throwstsc --emitDeclarationOnly a échoué (<tsconfig>). Plugins without a tsconfig emit no declarations.Copies
plugin.yamlintodist/, rewriting everymodule: *.tsto*.js.Copies
i18n/*.yamlandi18n/*.ymlintodist/i18n/.
Prints ✓ <name>@<version> → dist/ (<n> module(s) bundlé(s), deps runtime incluses).
See build and bundle and bundling and distribution.
publish
meridian-plugin publish [dir=.]… --registry <url> [--dry-run]
Publishes the dist/ of one or more built plugins to a plugin registry. Source:
libs/plugin-cli/src/commands/publish.ts; registry semantics in
libs/plugin-cli/src/registry/registry.ts, storage backends in
libs/plugin-cli/src/registry/store.ts.
| Argument / flag | Required | Default |
|---|---|---|
[dir]… |
no | . |
--registry <url> |
yes unless MERIDIAN_REGISTRY is set |
$MERIDIAN_REGISTRY |
--dry-run |
no | off |
Registry URL resolution: --registry <url> if present, else the
MERIDIAN_REGISTRY environment variable; neither present throws
registre non configuré : passez --registry <url> ou définissez MERIDIAN_REGISTRY.
See environment variables.
Behavior:
For each directory: if
<dir>/dist/plugin.yamlexists, publishes<dir>/dist; otherwise publishes<dir>directly. A directory withoutplugin.yamlthrows<dir> : pas de plugin.yaml — publiez le dossier COMPILÉ (dist/), pas la source.Orders the plugins by their
dependsOn: a dependency given on the command line comes before the plugins that cite it, the order given otherwise.Checks every plugin before writing anything, against the target registry plus, in memory, the plugins that come before it in the order (
overlayStore): the listings, the existence checks and the reads see them, so a dependency published in the same command counts for its dependents. For each plugin, it reads the publishedplugin.yamlagain. When itssdkrange accepts the CLI's plugin contract, or it has none:- the schema of that contract must accept the manifest: each error is printed as
- <path> : <message>(contributes.nodes[0].description : Required) and the plugin is refused with<name>@<version> : manifeste invalide pour le contrat plugin 0.1.8, publication refusée. Corriger le manifeste puis reconstruire.; - no key may be unknown to that schema, no
dependsOnentry may name the plugin itself (« N » se cite dans dependsOn. Retirer cette entrée puis reconstruire.), and eachdependsOnrange is checked against the registry: an invalid range (plage « R » de « D » invalide. Corriger dependsOn puis reconstruire.), or a dependency published on the registry whose complete versions are all outside the range (toutes les versions de « D » publiées sur <url> sont hors de la plage « R ». Publier une version de « D » dans « R » d'abord ou corriger la plage.), is a problem. A prerelease counts only for a range that names it, as in npm: a dependency published only as prereleases is outside^0.2.0. A*range is not checked. A dependency with no published version (a core plugin that comes from the image) is not checked either, and a note says so, under<name>@<version> : <n> note(s) de dépendances :(aucune version de « D » publiée sur <url> : plage « R » non contrôlée.). Each problem is printed as- <plugin> › <name> (<kind>) : <message>(kindkeywith the key's path, ordependencywith the dependency's name; same checks and messages asvalidatefor the keys), then✗ <name>@<version> : <n> problème(s) du manifeste, publication refusée.
A manifest whose range does not accept the contract belongs to a newer contract: it is published without these checks, since its shape, its keys and its dependencies may be that contract's; publish it with a CLI of that contract. A manifest without
nameorversionthen throws<path> : nom ou version manquant.- the schema of that contract must accept the manifest: each error is printed as
Refuses a version already published (versions are immutable):
<name>@<version> est déjà publié sur <url> — les versions sont immuables, incrémentez.When a plugin is refused, prints
✗ <n> plugin(s) refusé(s) sur <total>, rien n'est publié., writes nothing, for any plugin, and exits2.Otherwise uploads each plugin in the order, every
dist/file under@scope/name/<version>/…,plugin.yamllast as the completeness marker, and prints✓ <name>@<version> publié sur <url> (<n> fichier(s)).for each.With
--dry-run, runs the same checks, writes nothing and prints✓ <name>@<version> publiable sur <url> (<n> fichier(s), dry-run : rien n'est écrit).for each plugin; a refusal exits2, as without it.A registry error (network, credentials, a GCS error) throws:
✗ <message>, exit1. A plugin written before the error stays published.
| Outcome | Exit |
|---|---|
Every plugin published, or publishable with --dry-run |
0 |
| A plugin refused by the checks, nothing written | 2 |
| Registry, credential or usage error | 1 |
Registry backends
openStore (registry/store.ts) selects a backend from the URL:
| URL form | Backend |
|---|---|
/abs/path, ./rel/path, file:///… |
Local directory (dev, tests, NFS). |
gs://bucket[/prefix] |
Google Cloud Storage via REST JSON. |
The GCS backend authenticates with Application Default Credentials
(google-auth-library, scope devstorage.read_write, imported lazily) and uses
the ifGenerationMatch=0 precondition to enforce immutability
(HTTP 412 → clé déjà présente). See
set up a plugin registry
and declare and install plugins.
versions
meridian-plugin versions <@scope/name> --registry <url>
Lists the complete published versions of a plugin. Source:
libs/plugin-cli/src/commands/publish.ts (versionsCommand).
| Argument / flag | Required | Default |
|---|---|---|
<@scope/name> |
yes | — (first non-flag token that is not the --registry value) |
--registry <url> |
yes unless MERIDIAN_REGISTRY is set |
$MERIDIAN_REGISTRY |
Behavior:
- Missing name throws
usage: meridian-plugin versions <@scope/nom> --registry <url>. - Lists only versions whose
plugin.yamlmarker exists (interrupted publishes are invisible), sorted in ascending semver order via thesemverpackage; non-semver directory names sort to the end. - No versions: prints
<name> : aucune version publiée sur <url>. - Otherwise prints one version per line.
Environment variables
| Variable | Used by | Effect |
|---|---|---|
MERIDIAN_REGISTRY |
publish, versions |
Default registry URL when --registry is omitted; publish checks the dependsOn ranges against it and the plugins it publishes before. |
Google Cloud Storage registries additionally read the ambient ADC credentials
resolved by google-auth-library. Full instance-side variables are documented in
environment variables.
Related references
- Plugin manifest — the
plugin.yamlschema every command reads. - TypeRef — the type expressions codegen projects.
- SDK surface — the
@meridian/*imports left external at build time. - Behavior contract — the
NodeBehaviorFactorysignaturesgen-typestargets. - Workflow DSL — the workflow
validatechecks.