Documentation / Développement / Référence / meridian-plugin CLI

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) if dir/package.json exists.
  • Creates src/behaviors/ and workflows/ 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 dir plus its transitive dependsOn closure via standard Node resolution (require.resolve("<dep>/plugin.yaml") from the plugin folder), then the sibling directories (loadPluginClosure in libs/plugin-cli/src/discover.ts). A dependsOn package found by neither is skipped; validate, build and deps report it as absent.

  • Emits, into dir/generated/:

    File Emitted when Contents
    types.ts the plugin contributes ≥1 type one export interface per contributed type; import type lines for types owned by dependencies (<dep>/types).
    nodes.ts ≥1 node has a behavior PluginNodeIO (per-node inputs/outputs shapes) and the BehaviorFor<Id> alias. A declarative trigger (event: without behavior) has no entry.

    Declared events (contributes.events) generate no TypeScript: a coded trigger's fromEvent receives the event as Record<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 --plugin directory and its closure (deduped by name), sets aside a plugin whose sdk range does not accept the host's plugin contract (reported with kind contract, left out of the closures and of the pinned addresses, as the host does), reports for each other manifest, in the host's order, the sdk floor of its declared events and each key that the schema does not know, at any depth, with its path (kind key, manifestKeyIssues), then checks each dependsOn range against the version of the dependency found (kind dependency, 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 in node_modules nor 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 by contributedNodeMeta, 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 with Type de nœud « <id> » déjà enregistré (collision de plugin). (exit 1).

  • 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 --plugin flags; the conflict is reported to both plugins), the projection of each node (a node it refuses, such as a trigger without pinnedRouting and without event, is reported and not registered; a defaultSelector on a node, and a host transport (http, directory) in a receive.transports, are reported and ignored), bindings to a declared event (the map and require of an event binding, the require of a message binding), and the sdk range of a manifest that uses keys of a later contract (minimum 0.1.1 for declared events, 0.1.3 with an event defaultSelector, 0.1.4 when a node describes its reception, 0.1.5 with a conditional port rule, 0.1.6 with connections, 0.1.7 with a node connection by port, urlSchemes or exposeCredentials, 0.1.8 with the connection of 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 event defaultSelector that reads a field outside the event's contract) is printed the same way under plugins : <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 the dependsOn closure, neither given with --plugin nor the current directory, is a note too, printed under dé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 exit 1.

  • Parses the workflow (WorkflowSpec.parse) and runs validateSpec, 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 dir and its closure like validate (node_modules, then the sibling directories), applies the contract check to the plugins found, and runs the dependsOn check of validate and build (dependencyIssues). The plugin at dir has its ranges checked whatever its own contract.

  • Prints <name>@<version> : <n> dépendance(s) (or aucune 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 plage
    
    State DependencyState Meaning Problem
    ok ok The version found is in the range. no
    sans contrainte unconstrained The range is * (or empty). no
    hors plage out-of-range The version found is outside the range: the plugin is refused. yes
    plage invalide invalid-range The range is not a semver range: the plugin is refused. yes
    absente absent Found neither in node_modules nor in a sibling directory (version and origin —). yes
    refusée refused Found with a version in the range, but refused for its contract or one of its own entries. yes
    auto-référence self The entry names the plugin itself: the plugin is refused. yes

    The second column is the state dependencyIssues returns (@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 as validate prints 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 dir is 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ée dependency, prints the problems that refuse it (its contract, or one of its own ranges) under dépendances : <n> note(s) de refus :.

  • Exits 1 when the plugin is refused for its contract or has a dependency problem, 0 otherwise.

  • 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 --locale throws usage: 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 exits 0.
  • For each bundle (sorted by filename):
    • The source locale (bundle name equal to manifest.locale) is exempt from manifest keys; only its runtime section 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.
  • 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:

  1. Loads the plugin's dependsOn closure and runs the load checks of validate on it; a problem of the built plugin itself is printed, an unknown key of its manifest, a dependsOn range 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 of validate, and the problems that refuse a refusée dependency are printed under dépendances : <n> note(s) de refus :, as deps prints them. A note is printed under <name> : <n> note(s) d'événements déclarés : and the build goes on.

  2. Collects every module: referenced by the manifest — node behavior, receive and assist modules, port adapters, event sources, message bindings and port sandboxes (manifestModules) — and resolves each to its source (./x.ts, or ./x.js with source under src/; resolveEntry). An unresolvable module throws <root> : module du manifeste introuvable : <module> ….

  3. Removes and recreates dist/.

  4. If there are modules, runs esbuild with these settings:

    esbuild option Value
    bundle true
    splitting true (shared code → chunks/[name]-[hash])
    format esm
    platform node
    target node20
    external ["node:*", "@meridian/*"] — Node builtins and the SDK stay external (provided by the host)
    banner.js ESM-compat shim defining require, __filename, __dirname
    logLevel warning

    outbase is src/ when all entries live under src/ (scaffold layout), else the plugin root. A non-empty esbuild error list throws esbuild : <n> erreur(s).

  5. If a tsconfig.build.json or tsconfig.json exists, runs npx tsc -p <tsconfig> --emitDeclarationOnly --noCheck for the ./types export consumed by dependent plugins. A non-zero status throws tsc --emitDeclarationOnly a échoué (<tsconfig>). Plugins without a tsconfig emit no declarations.

  6. Copies plugin.yaml into dist/, rewriting every module: *.ts to *.js.

  7. Copies i18n/*.yaml and i18n/*.yml into dist/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.yaml exists, publishes <dir>/dist; otherwise publishes <dir> directly. A directory without plugin.yaml throws <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 published plugin.yaml again. When its sdk range 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 dependsOn entry may name the plugin itself (« N » se cite dans dependsOn. Retirer cette entrée puis reconstruire.), and each dependsOn range 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> (kind key with the key's path, or dependency with the dependency's name; same checks and messages as validate for 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 name or version then throws <path> : nom ou version manquant.

  • 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 exits 2.

  • Otherwise uploads each plugin in the order, every dist/ file under @scope/name/<version>/…, plugin.yaml last 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 exits 2, as without it.

  • A registry error (network, credentials, a GCS error) throws: ✗ <message>, exit 1. 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.yaml marker exists (interrupted publishes are invisible), sorted in ascending semver order via the semver package; 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.

97 documents14 sectionssource : /docs · généré au build