# @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 ```sh npx @emdash-cms/plugin-cli init my-plugin ``` Or install globally: ```sh npm install -g @emdash-cms/plugin-cli emdash-plugin init my-plugin ``` ## Commands ```text 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 Publish a release that points at a hosted tarball emdash-plugin validate [path] Validate emdash-plugin.jsonc against the v1 schema emdash-plugin login Interactive atproto OAuth login emdash-plugin logout [--did ] Revoke the active session emdash-plugin whoami Show stored sessions emdash-plugin switch Switch the active publisher session emdash-plugin search Free-text search emdash-plugin info 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 ` (or `EMDASH_REGISTRY_URL`). ## Development From a fresh checkout, run `pnpm install` and then `pnpm build` before a scoped package typecheck: ```sh 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: ```json { "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. ```sh emdash-plugin bundle # upload dist/-.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: ```jsonc { "$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`: ```jsonc { "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 ` - 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: ```sh 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 ```ts 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`.