Files
emdash-cms--emdash/packages/plugin-cli
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00
..

@emdash-cms/plugin-cli

CLI for authoring, building, and publishing EmDash plugins.

EXPERIMENTAL: init, build, dev, bundle, login, whoami, switch, and publish all work today against any atproto PDS — publish writes profile + release records to the publisher's own repo. The discovery commands (search, info) need an aggregator; the experimental aggregator is at registry.emdashcms.com. NSIDs and shapes will change while RFC 0001 is in flight; pin to an exact version.

Installation

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

Or install globally:

npm install -g @emdash-cms/plugin-cli
emdash-plugin init my-plugin

Commands

emdash-plugin init [name]                    Scaffold a new sandboxed plugin
emdash-plugin build                          Build dist/ artifacts (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 publish --url <url>            Publish a release that points at a hosted tarball
emdash-plugin validate [path]                Validate emdash-plugin.jsonc against the v1 schema
emdash-plugin login <handle-or-did>          Interactive atproto OAuth login
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 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).

Development

From a fresh checkout, run pnpm install and then pnpm build before a scoped package typecheck:

pnpm install
pnpm build
pnpm --filter @emdash-cms/plugin-cli typecheck

The build emits internal workspace type declarations used by the scoped typecheck, matching CI's build-then-typecheck order.

Authoring

A typical plugin's package.json scripts:

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

The plugin author writes two files:

  • emdash-plugin.jsonc — identity (slug, publisher) + trust contract (capabilities, allowedHosts, storage) + profile fields.
  • src/plugin.ts — runtime behaviour (hooks + routes), with export default { ... } satisfies SandboxedPlugin from emdash/plugin.

emdash-plugin build produces:

  • dist/plugin.mjs (+ dist/plugin.d.mts) — runtime bytes the integration loads (in-process or in a sandbox isolate).
  • dist/manifest.json — wire-shape manifest including the hooks + routes harvested from probing src/plugin.ts.
  • dist/index.mjs (+ dist/index.d.mts) — descriptor module that default-exports a bare PluginDescriptor. Consumers import this directly.

Publishing

Three steps. The CLI does not host artifacts — you do, anywhere public.

emdash-plugin bundle
# upload dist/<id>-<version>.tar.gz somewhere public
emdash-plugin publish --url https://example.com/foo-1.0.0.tar.gz

On first publish, pass --license and --security-email (or --security-url) to bootstrap the package profile — or keep them in emdash-plugin.jsonc (see below).

emdash-plugin.jsonc

Drop an emdash-plugin.jsonc file next to your plugin's package.json. The CLI reads it automatically from the current directory. Schema-driven IDE completion works via the bundled JSON Schema:

{
	"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",

	"slug": "gallery",
	"publisher": "did:plc:abc123def456",

	"license": "MIT",
	"author": { "name": "Jane Doe", "url": "https://example.com" },
	"security": { "email": "security@example.com" },

	// Optional
	"name": "Gallery",
	"description": "Image gallery block for EmDash.",
	"keywords": ["gallery", "images"],
	"repo": "https://github.com/example/plugin-gallery",

	// Trust contract
	"capabilities": ["content:read"],
	"allowedHosts": [],
	"storage": {},
}

The file is JSONC: comments and trailing commas are allowed. Use authors: [...] and securityContacts: [...] for multi-author or multi-contact plugins. version is optional — when omitted, the CLI reads version from the adjacent package.json.

Publisher pinning

After your first successful publish, the CLI writes the active session's DID back into the manifest as publisher:

{
	"license": "MIT",
	"publisher": "did:plc:abc123def456",
	...
}

On every subsequent publish, the CLI verifies the active session matches the pinned publisher. If they don't match, publish refuses with MANIFEST_PUBLISHER_MISMATCH so you can't accidentally publish under the wrong account. To resolve a mismatch, either:

  • switch sessions: emdash-plugin switch <did>
  • update the manifest if you're transferring the plugin to a new publisher

DIDs are the identity, not handles. Internally the CLI always compares the active session's DID against the pinned publisher's DID. If you pin a handle ("publisher": "example.com"), the CLI resolves it to a DID at publish time and compares against that — so a handle pin is just a friendlier alias for the underlying DID. Handles are mutable: if the publisher's domain changes ownership and the resolver later points at a different DID, the publish will refuse. DIDs are durable and the recommended pin for long-lived plugins.

Validate without publishing:

emdash-plugin validate

CLI flags (--license, --author-name, …) still win over manifest values when both are set, which is useful in CI. Pass --no-manifest to skip the manifest entirely.

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 credentials, import from @emdash-cms/registry-client.