# Migrating to the plugin CLI

> Breaking changes for sandboxed-plugin authors, and how to update your plugin.

Source: https://emdashcmseverything.com/official-docs/plugins/creating-plugins/migrating-to-the-cli/

This guide is for authors of **sandboxed** plugins written against the previous `definePlugin()` shape. Work through the breaking changes in order. None of them change how your hooks or routes behave at runtime; they change how the plugin is declared, built, and published.

> **Native plugins are unaffected**
>
> Native plugins continue to use `definePlugin()` from `emdash` and the
> `PluginDefinition` shape. Nothing on this page applies to them.

For the full list of changes in each package, see its entry on the [releases page](https://github.com/emdash-cms/emdash/releases).

## Breaking changes

### Renamed: `@emdash-cms/registry-cli` is now `@emdash-cms/plugin-cli`

Earlier releases shipped the CLI as `@emdash-cms/registry-cli`, with an `emdash-registry` binary.

The package is now `@emdash-cms/plugin-cli` and the binary is `emdash-plugin`. The old package is no longer published.

#### What should I do?

Replace the dependency:

```sh
pnpm remove @emdash-cms/registry-cli
pnpm add -D @emdash-cms/plugin-cli
```

Replace `emdash-registry` with `emdash-plugin` everywhere you call it. Every subcommand keeps its name (`bundle`, `publish`, `login`, `whoami`, `switch`, `validate`), and `init`, `build`, and `dev` are added. See [The plugin CLI](https://emdashcmseverything.com/official-docs/plugins/creating-plugins/cli/).

### Renamed: capability names use resource-first spelling

Earlier manifests used capability names such as `read:content` and `network:fetch`. The authoring manifest accepts only the current names, although the runtime still normalizes legacy names in already-published bundles during the compatibility window.

#### What should I do?

Replace every legacy name in the manifest:

| Earlier name | Current name |
| --- | --- |
| `network:fetch` | `network:request` |
| `network:fetch:any` | `network:request:unrestricted` |
| `read:content` | `content:read` |
| `write:content` | `content:write` |
| `read:media` | `media:read` |
| `write:media` | `media:write` |
| `read:users` | `users:read` |
| `email:provide` | `hooks.email-transport:register` |
| `email:intercept` | `hooks.email-events:register` |
| `page:inject` | `hooks.page-fragments:register` |

Use `network:request` with a non-empty `allowedHosts` list. Use `network:request:unrestricted` with an empty list only when an operator chooses the destination at runtime. [Capabilities and security](https://emdashcmseverything.com/official-docs/plugins/creating-plugins/capabilities/) explains the current permissions and network rules.

### Changed: sandboxed plugins use an explicit `SandboxedPlugin` annotation

Earlier releases wrapped the plugin's hooks and routes in `definePlugin()` imported from `emdash`, with each handler's parameters annotated by hand.

A sandboxed plugin assigns its definition to a `SandboxedPlugin`-typed constant and exports that constant as default. Import the type from `emdash/plugin` with `import type`; the bundler erases that import. The same subpath also exports the lightweight `pluginRoute()` and `pluginResponse()` runtime helpers. TypeScript infers each handler's `event` and `ctx` from the hook or route name, so handler parameters need no annotations. The explicit annotation also keeps generated declarations portable under isolated package-manager layouts.

#### What should I do?

Make four changes to the plugin's source file. Replace the import:

```ts del={1} ins={2}
import { definePlugin, type ContentHookEvent, type PluginContext } from "emdash";
import type { SandboxedPlugin } from "emdash/plugin";
```

Replace the `definePlugin()` wrapper with an explicitly typed constant:

```ts del={1} ins={2-4}
export default definePlugin({ /* hooks, routes */ });
const plugin: SandboxedPlugin = { /* hooks, routes */ };

export default plugin;
```

Remove the parameter annotations from every handler:

```ts del={1} ins={2}
handler: async (event: ContentHookEvent, ctx: PluginContext) => {
handler: async (event, ctx) => {
```

The result is one default-exported object:

```ts title="src/plugin.ts"
import type { SandboxedPlugin } from "emdash/plugin";

const plugin: SandboxedPlugin = {
	hooks: {
		"content:beforeSave": {
			handler: async (event, ctx) => {
				return event.content;
			},
		},
	},
};

export default plugin;
```

To name an event type in a helper function, import it from `emdash/plugin`:

```ts
import type { ContentHookEvent, PluginContext } from "emdash/plugin";
```

A handler's `event` is always the canonical type for that hook. Annotating a handler with a narrower interface no longer type-checks. Validate any fields you depend on at runtime with a `typeof` check or a guard, which is the correct approach for data that comes from outside the type system.

### Changed: a plugin is one `src/plugin.ts` plus `emdash-plugin.jsonc`

Earlier releases split a plugin into two files: `src/index.ts` returned a `PluginDescriptor` (id, version, capabilities, storage, entrypoint), and `src/sandbox-entry.ts` held the hooks and routes.

A plugin is now one runtime file, `src/plugin.ts` (hooks and routes), and one hand-edited manifest, `emdash-plugin.jsonc` (identity and the trust contract). The `entrypoint` and `format` fields are gone; the build wires them up.

#### What should I do?

Move the hooks and routes into `src/plugin.ts` using the shape above. Move the descriptor's metadata into `emdash-plugin.jsonc` next to `package.json`. The descriptor `id` becomes the manifest `slug`; `capabilities`, `allowedHosts`, and `storage` keep their shape; `version` is read from `package.json`, so omit it.

The following example shows the manifest equivalent of a descriptor that declared one storage collection:

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

	"slug": "plugin-hello",
	"publisher": "did:plc:abc123def456",

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

	"capabilities": [],
	"allowedHosts": [],
	"storage": { "events": { "indexes": ["timestamp"] } }
}
```

See [The plugin manifest](https://emdashcmseverything.com/official-docs/plugins/creating-plugins/manifest/) for every field, and [Publisher pinning](https://emdashcmseverything.com/official-docs/plugins/creating-plugins/manifest/#publisher-pinning) for the `publisher` field.

In `package.json`, point the `"./sandbox"` export at the built runtime file:

```json del={1} ins={2}
"./sandbox": "./dist/sandbox-entry.mjs"
"./sandbox": "./dist/plugin.mjs"
```

Add the manifest to `files` so it ships with the package:

```json del={1} ins={2}
"files": ["dist"]
"files": ["dist", "emdash-plugin.jsonc"]
```

### Changed: build with `emdash-plugin build`

Earlier releases built the two source files with a hand-written `tsdown` script.

`emdash-plugin build` reads `emdash-plugin.jsonc` and `src/plugin.ts` and emits the `dist/` artifacts. `emdash-plugin dev` watches and rebuilds.

#### What should I do?

Replace the build script and add a watch script:

```json title="package.json" del={2} ins={3,4}
"scripts": {
	"build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean"
	"build": "emdash-plugin build",
	"dev": "emdash-plugin dev"
}
```

Then validate and build:

```sh
emdash-plugin validate
emdash-plugin build
```

### Removed: standard-format type and function exports from `emdash`

Earlier releases exported `StandardPluginDefinition`, `StandardHookHandler`, `StandardHookEntry`, `StandardRouteHandler`, `StandardRouteEntry`, and the function `isStandardPluginDefinition` from `emdash`.

These are removed. They were helper aliases for the previous `definePlugin` shape.

#### What should I do?

Use `SandboxedPlugin` from `emdash/plugin` for the same purpose. A sandboxed plugin's exported definition is already typed by its `SandboxedPlugin` annotation, so there is no replacement for `isStandardPluginDefinition`; identify a plugin by its structure (`{ hooks?, routes? }`) if you need to.

### Renamed: sandbox-runner handles use `SandboxedPluginInstance`

This affects only authors of a custom `SandboxRunner`, such as `@emdash-cms/cloudflare`. Most plugin authors can skip it.

The author-facing `SandboxedPlugin` type is available from the `emdash/plugin` authoring entry point. The runtime handle returned by `SandboxRunner.load` is exported from `emdash` as `SandboxedPluginInstance`.

#### What should I do?

If you import `SandboxedPlugin` from `emdash` to type a sandbox runner or hold runtime plugin handles, change the import to `SandboxedPluginInstance`:

```ts del={1} ins={2}
import type { SandboxedPlugin } from "emdash";
import type { SandboxedPluginInstance } from "emdash";
```

## Tell your users

Sites that install your plugin also need to change their import. Point them at the new shape: drop the braces and the `()`.

```js title="astro.config.mjs" del={1,7} ins={2,8}
import { helloPlugin } from "@my-org/plugin-hello";
import hello from "@my-org/plugin-hello";

export default defineConfig({
	integrations: [
		emdash({
			sandboxed: [helloPlugin()],
			sandboxed: [hello],
		}),
	],
});
```

If your plugin accepted configuration through its factory, move that configuration to an admin settings page and read it from `ctx.settings`. Sandboxed plugin descriptors are plain objects and cannot receive constructor options. See [Settings](https://emdashcmseverything.com/official-docs/plugins/creating-plugins/settings/).

## Verify the migrated plugin

Run the plugin's tests, validate the authoring manifest, and execute the complete build and bundle checks:

```sh
pnpm test
pnpm exec emdash-plugin validate
pnpm exec emdash-plugin build
pnpm exec emdash-plugin bundle --validate-only
```

Then install the local package into a development site and exercise each migrated hook and route. The build can confirm their names and shapes, but it cannot confirm that a route returns the intended data or that a hook preserves content correctly.

## Next

- [The plugin manifest](https://emdashcmseverything.com/official-docs/plugins/creating-plugins/manifest/)
- [The plugin CLI](https://emdashcmseverything.com/official-docs/plugins/creating-plugins/cli/)
- [Your first sandboxed plugin](https://emdashcmseverything.com/official-docs/plugins/creating-plugins/your-first-plugin/)
