The `emdash-plugin` CLI

On this page

@emdash-cms/plugin-cli scaffolds, builds, validates, and publishes sandboxed plugins. It also manages publisher sign-in, package profiles, registry discovery, and automated releases. The installed binary is emdash-plugin.

The CLI uses an Atmosphere account as the publisher identity for package profiles and releases.

Commands

The CLI provides the following commands:

emdash-plugin init [name]                    Scaffold a new sandboxed plugin
emdash-plugin build                          Build dist/ (plugin.mjs, manifest.json, index.mjs)
emdash-plugin dev                            Watch sources and rebuild on change
emdash-plugin bundle                         Pack dist/ + assets into a registry tarball
emdash-plugin validate [path]                Validate emdash-plugin.jsonc against the schema
emdash-plugin publish                        Build, upload, and publish a release
emdash-plugin update-package [--yes]         Preview or apply package-profile changes
emdash-plugin profile setup                  Prepare the signed package profile for delegated releases
emdash-plugin release setup                  Create the delegated-release GitHub Actions workflow
emdash-plugin release plan                   Plan repository releases for GitHub Actions
emdash-plugin release prepare <slug[@ver]>   Prepare one repository package for GitHub Actions
emdash-plugin login <handle-or-did>          Sign in with your Atmosphere account
emdash-plugin logout [--did <did>]           Revoke the active session
emdash-plugin whoami                         Show stored sessions
emdash-plugin switch <did>                   Switch the active publisher session
emdash-plugin search <query>                 Free-text registry search
emdash-plugin info <handle-or-did> <slug>    Show package details

Run emdash-plugin <command> --help for the current arguments and flags. Commands intended for scripts, including validate, publish, update-package, search, info, login, and whoami, provide JSON output where their help lists --json. Discovery commands accept --registry-url <url> or the EMDASH_REGISTRY_URL environment variable.

The following example shows the two scripts most plugins add to package.json:

{
	"scripts": {
		"build": "emdash-plugin build",
		"dev": "emdash-plugin dev"
	}
}

init

Create a new plugin with init:

npx @emdash-cms/plugin-cli init my-plugin

This scaffolds emdash-plugin.jsonc, src/plugin.ts, package.json, tsconfig.json, vitest.config.ts, a workerd-backed test, a README, AGENTS.md, a local creating-plugins skill, and package-manager configuration. .agents/skills and .claude/skills link to the canonical skills directory, and .claude/CLAUDE.md links to AGENTS.md, so Codex and Claude use the same project guidance. The source starts with one route assigned to a SandboxedPlugin-typed constant and exported as default. The test invokes that route through EmDash’s production sandbox wrapper and host bridge.

Interactive setup asks for the publisher, author, security contact, and source repository, then shows the complete project summary before writing. Required fields cannot be skipped.

The CLI detects whether npm, pnpm, Yarn, or Bun launched it and generates matching commands. Override the choice with --package-manager. A pnpm scaffold includes the reviewed build-script policy needed by esbuild.

Non-interactive setup requires explicit ownership metadata. Use the following form in scripts:

npx @emdash-cms/plugin-cli init my-plugin --yes \
  --publisher did:plc:abc123def456 \
  --author-name "Jane Doe" \
  --security-email [email protected]

Pass --use-detected to opt into the active publisher session and local Git author or repository metadata. Without that flag, --yes does not copy identity-bearing local defaults.

build

build reads emdash-plugin.jsonc, src/plugin.ts, and an optional sibling package.json, and emits the following files:

ArtifactWhat it is
dist/plugin.mjs (+ dist/plugin.d.mts)The hooks and routes. Loaded in-process (plugins: []) and by the sandbox loader (sandboxed: []).
dist/manifest.jsonThe plugin’s manifest, including the hooks and routes read from src/plugin.ts. bundle includes this file as-is; npm consumers read it without parsing the JSONC source.
dist/index.mjs (+ dist/index.d.mts)The descriptor module a site imports in astro.config.mjs. Emitted only when a sibling package.json exists; registry-only plugins skip it, since nothing imports it.

dist/ is build output. Do not commit it. The scaffold’s .gitignore excludes it. Run emdash-plugin build before packing or publishing the npm package so its files list has the generated artifacts to include.

dev

Watches src/**, emdash-plugin.jsonc, and package.json, debouncing rebuilds at 150 ms. Rebuilds are serialised. On a failed rebuild it leaves the last good dist/ in place, so a site importing the plugin via a workspace/file link keeps working until the next successful build. Ctrl-C drains cleanly.

Develop against a real site by running pnpm dev in the plugin directory and installing it into the site with pnpm add file:../path/to/plugin. Import the plugin’s default export into emdash({ sandboxed: [...] }). The first-plugin tutorial shows the complete setup.

validate

Validate the manifest in the current directory, or pass a different plugin directory:

emdash-plugin validate          # ./emdash-plugin.jsonc
emdash-plugin validate path/    # a specific directory

Offline schema check with tsc-style file:line:column diagnostics, including the manifest’s cross-field rules. No network. Good as a pre-commit or CI gate. See the manifest reference.

bundle

bundle is a thin packaging step on top of build:

  1. Runs build to produce dist/.
  2. Validates the bundle: no Node-builtin imports, no oversized files, capability sanity.
  3. Collects optional assets — README, icon, screenshots.
  4. Tarballs. Inside the tarball, plugin.mjs is packed as backend.js (the filename the registry expects). The output is dist/<slug>-<version>.tar.gz.

--validate-only skips tarball creation but still produces the dist/ artifacts — “validate” implies “build first”.

publish

publish builds and validates the plugin, uploads the package and listing images to your PDS, then writes the release record.

emdash-plugin login alice.example.com
emdash-plugin publish

publish reads the manifest for profile fields and enforces publisher pinning. Keep the license, author, security contact, and other package information in the manifest. The older profile flags and --no-manifest remain available for legacy scripted publishing; check publish --help before maintaining one of those flows.

Pass --url <https-url> to use an externally hosted package bundle. The CLI downloads and validates the URL before publishing. Add --local <path> to verify that a local tarball matches the downloaded bytes.

Follow Bundling and publishing for the complete local release flow.

update-package

Use update-package to change an existing package profile without creating a release. It reads the profile fields in emdash-plugin.jsonc, fetches the current signed profile, and prints the proposed changes:

emdash-plugin update-package

The command is a dry run unless you pass --yes:

emdash-plugin update-package --yes

The write uses the current record CID as a precondition. If another process changes the profile after the command reads it, the update fails with STALE_RECORD instead of overwriting the newer record. Removing an optional property from the manifest leaves its published value unchanged; set the intended replacement explicitly.

profile setup

profile setup prepares the publisher-owned package profile for automated releases. It creates a missing profile from emdash-plugin.jsonc, or adds delegated-release settings to an existing valid profile without replacing its package metadata.

Run the interactive setup from the plugin directory:

emdash-plugin profile setup
FlagDefaultDescription
--dir <path>Current directoryPlugin source directory.
--repository <url>Manifest repoCanonical public GitHub repository URL. Interactive setup asks when neither value is present.
--confirmation <mode>escalation-onlyUse escalation-only for permission increases or always for every release.
--yes, -yfalseAccept the default policy without prompting. Required when a non-interactive run would change the profile.

The command uses the active CLI login to write the profile. It refuses to replace a different signed repository. Run emdash-plugin switch <did> when the active account does not match the manifest publisher.

release setup

release setup runs package-profile setup, then creates one shared .github/workflows/emdash-release.yml at the Git repository root. Nested plugin packages reuse the same workflow.

emdash-plugin release setup

It accepts the profile setup flags plus the following workflow options:

FlagDefaultDescription
--service-url <origin>https://releases.emdashcms.comHTTPS origin used by the generated Action.
--action-ref <ref>mainEmDash repository ref containing the release Action.
--trigger <mode>autoRelease source: changesets, tags, or manual. auto offers Changesets when .changeset/config.json exists.
--forcefalseReplace an existing generated workflow. Without it, setup leaves the existing file unchanged.

When setup detects Changesets in an interactive terminal, it asks how EmDash plugins should be released. Follow Changesets releases publishes the same versions for packages containing emdash-plugin.jsonc. The other choices follow <slug>@<version> tags or allow manual runs only. In non-interactive use, auto selects Changesets when a valid root configuration exists and package tags otherwise.

The Changesets variant is a reusable workflow. Add one caller job after the existing Changesets publish job and pass its official published-package JSON output. Private EmDash-only packages require privatePackages.version: true and privatePackages.tag: true; setup warns when either option is missing.

The command never pushes the generated workflow. The first automated run creates a repository connection request using GitHub OpenID Connect; no Actions secret is required. Follow Automated plugin releases to review the workflow, authorise the release service, connect the repository, and publish the first release.

release plan

release plan is used by the generated workflow. With --published-packages <json>, it maps the Changesets Action output to packages containing emdash-plugin.jsonc, verifies their versions, and writes a JSON selector matrix to GITHUB_OUTPUT. With --package <slug[@version]>, it validates one manual selector. The command does not build or publish packages.

release prepare

release prepare is the generated workflow’s package resolver. It finds one plugin manifest in the repository, checks an optional tag version, builds the package, and writes its package, publisher, directory, and bundle outputs to GITHUB_OUTPUT.

The generated workflow passes a package tag automatically:

emdash-plugin release prepare [email protected]

Pass a plain plugin ID for a manual workflow run. The command uses the version from that package’s manifest. Duplicate plugin IDs, missing packages, and version mismatches fail before provenance is created.

Programmatic API

Build or bundle a plugin from Node.js by importing the CLI’s programmatic functions:

import { buildPlugin, bundlePlugin } from "@emdash-cms/plugin-cli";

await buildPlugin({ dir: "./my-plugin" });
const result = await bundlePlugin({ dir: "./my-plugin" });

For discovery and credential helpers, import from @emdash-cms/registry-client.