cli: prompt for plugin format and align scaffolds with docs#902
cli: prompt for plugin format and align scaffolds with docs#902
Conversation
`emdash plugin init` now asks whether to scaffold a sandboxed or native plugin when run interactively. `--format=sandboxed`, `--format=native`, and the existing `--native` flag all skip the prompt; non-TTY runs default to sandboxed. Both scaffolds now match the canonical patterns from the plugin docs: they build to `dist/` via tsdown, declare a sample storage collection, and demonstrate a hook plus an API route. The sandboxed entry uses an explicitly typed `ContentSaveEvent`; the native entry forwards options through `createPlugin`. The descriptor `id` is derived from the slug instead of the full scoped package name, so scoped names like `@org/my-plugin` produce a runtime-valid id.
🦋 Changeset detectedLatest commit: 448c232 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-perf-coordinator | 448c232 | May 03 2026, 07:04 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-i18n | 448c232 | May 03 2026, 07:04 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 448c232 | May 03 2026, 07:05 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 448c232 | May 03 2026, 07:06 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 448c232 | May 03 2026, 07:06 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
Pull request overview
This PR updates emdash plugin init so new plugin authors can choose between sandboxed and native plugin formats up front, and refreshes the generated scaffolds/build metadata to better match current plugin packaging patterns in the codebase.
Changes:
- Adds
--formatsupport plus an interactive format prompt for TTY runs, while keeping non-interactive defaults unchanged. - Reworks both generated plugin templates to emit
dist/-based packages with tsdown/typecheck scripts and richer sample hook/route/storage code. - Fixes scaffolded descriptor IDs for scoped package names by deriving plugin IDs from the unscoped slug.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
packages/core/src/cli/commands/plugin-init.ts |
Adds format selection logic and rewrites the generated sandboxed/native plugin scaffolds. |
.changeset/plugin-init-format-prompt.md |
Documents the new prompt behavior, scaffold updates, and scoped-name ID fix for release notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| \tenabled?: boolean; | ||
| } | ||
|
|
||
| export function ${fnName}Plugin(options: ${typeName}Options = {}): PluginDescriptor { |
| \t\tformat: "standard", | ||
| \t\tentrypoint: "${pluginName}/sandbox", | ||
|
|
||
| \t\tcapabilities: [], |
| \troutes: { | ||
| \t\trecent: { | ||
| \t\t\thandler: async (_routeCtx, ctx: PluginContext) => { | ||
| \t\t\t\tconst result = await ctx.storage.events.query({ limit: 10 }); |
| \treturn definePlugin({ | ||
| \t\tid: "${pluginName}", | ||
| \t\tid: "${slug}", | ||
| \t\tversion: "0.1.0", |
| \t\troutes: { | ||
| \t\t\trecent: { | ||
| \t\t\t\thandler: async (ctx) => { | ||
| \t\t\t\t\tconst result = await ctx.storage.events.query({ limit: 10 }); |
| if (formatArg) { | ||
| const normalized = formatArg.toLowerCase(); | ||
| if (normalized === "native") return "native"; | ||
| if (normalized === "sandboxed" || normalized === "standard") return "standard"; | ||
| consola.error(`Invalid --format "${formatArg}". Use "sandboxed" or "native".`); | ||
| process.exit(1); | ||
| } | ||
| if (nativeFlag) return "native"; |
- Both scaffolded hooks declare `content:read` so HookPipeline does not
silently skip them (afterSave requires `content:read`).
- The native descriptor factory now returns
`PluginDescriptor<${Type}Options>` so the options type survives,
matching first-party plugins like ai-moderation and embeds.
- The `recent` routes pass `orderBy: { ...: "desc" }`; storage queries
default to `created_at ASC`, which returned the oldest events.
- `--native --format=sandboxed` (or any conflicting pair) now errors
with exit 1 instead of silently picking the format flag.
What does this PR do?
emdash plugin initpreviously always scaffolded the standard (sandboxed) format, with--nativeas the only escape hatch. Users who didn't already know the two formats existed had no signal that they had a choice.This PR:
--formatnor--nativeis set. Options are Sandboxed (default; "runs in an isolated sandbox; safe to install from the marketplace") and Native ("full runtime access; install from npm").--format=sandboxed/--format=nativeto skip the prompt explicitly.--nativeis kept as a shortcut for--format=native. Non-TTY runs continue to default to sandboxed, so scripted usage is unchanged.docs/src/content/docs/plugins/creating-plugins/your-first-plugin.mdxandcreating-native-plugins/your-first-native-plugin.mdx:package.jsonships a tsdown build (build/dev/typecheckscripts),main+ conditionalexportspointing atdist/,files: ["dist"],keywords,license: "MIT", anddevDependenciesforemdash/tsdown/typescript.src/sandbox-entry.tsdropsevent: any, defines a typedContentSaveEvent, writes toctx.storage.events, and demonstrates arecentroute using the standard two-arg(routeCtx, ctx)signature.src/index.tsadds anOptionsinterface, forwards the options throughcreatePlugin, declares storage, demonstrates an options-aware hook, and exposes arecentroute using the native single-arg(ctx)signature.idwas being set to the rawpluginName, which fails the runtime regex/^[a-z][a-z0-9_-]*$/when given a scoped name like@org/foo. It now uses the stripped slug.pnpm buildand skipemdash plugin validatefor native plugins (validate is part of the marketplace bundle flow, which only applies to sandboxed plugins).Closes #
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
Smoke-tested locally by scaffolding both formats with a scoped name (
@my-org/test-sandboxedand@my-org/test-native) and verifying the generatedpackage.json,tsconfig.json, andsrc/*.tsfiles match the canonical examples from the docs and produce a runtime-valid descriptorid.