@emdash-cms/plugin-cli is the authoring toolchain: scaffold, build, watch, validate, bundle, publish, plus identity and discovery. The 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 profile setup Prepare the signed package profile for delegated releases
emdash-plugin release setup Create the delegated-release GitHub Actions workflow
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
The non-interactive output commands (whoami, validate, search, info, login, publish) accept --json for machine-readable output. Discovery commands (search, info) accept --registry-url <url> (or EMDASH_REGISTRY_URL).
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 a self-contained plugin: emdash-plugin.jsonc, src/plugin.ts (one example route in the satisfies SandboxedPlugin shape), package.json, tsconfig.json, a test, a README, and .gitignore. A slug is the only required input. A scaffold created from just a slug is a valid starting point: the manifest carries TODO: comments for the few fields to fill in — publisher, author, and security contact — before the plugin will load or publish.
build
build reads emdash-plugin.jsonc, src/plugin.ts, and an optional sibling package.json, and emits the following files:
| Artifact | What 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.json | The 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, and installs rebuild it.
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 here and pnpm add file:../path/to/this in the site, then importing the plugin’s default export into emdash({ sandboxed: [...] }).
validate
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:
- Runs
buildto producedist/. - Validates the bundle: no Node-builtin imports, no oversized files, capability sanity.
- Collects optional assets — README, icon, screenshots.
- Tarballs. Inside the tarball,
plugin.mjsis packed asbackend.js(the filename the registry expects). The output isdist/<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. On first publish, pass --license and a security contact, or keep them in the manifest. Explicit flags override manifest values; --no-manifest opts out entirely.
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.
Full walkthrough: Bundling and publishing.
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
| Flag | Default | Description |
|---|---|---|
--dir <path> | Current directory | Plugin source directory. |
--repository <url> | Manifest repo | Canonical public GitHub repository URL. Interactive setup asks when neither value is present. |
--confirmation <mode> | escalation-only | Use escalation-only for permission increases or always for every release. |
--yes, -y | false | Accept 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 .github/workflows/emdash-release.yml for the release service.
emdash-plugin release setup
It accepts the profile setup flags plus the following workflow options:
| Flag | Default | Description |
|---|---|---|
--service-url <origin> | https://releases.emdashcms.com | HTTPS origin used by the generated Action. |
--action-ref <ref> | main | EmDash repository ref containing the release Action. |
--force | false | Replace an existing generated workflow. Without it, setup leaves the existing file unchanged. |
The command never pushes the generated workflow. Follow Automated plugin releases to authorise the release service, connect the workflow, and publish the first release.
Programmatic API
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.