Skip to content

feat(plugin-cli): sandboxed plugin authoring CLI#1057

Merged
ascorbic merged 17 commits into
mainfrom
feat/sandboxed-plugin-redesign
May 18, 2026
Merged

feat(plugin-cli): sandboxed plugin authoring CLI#1057
ascorbic merged 17 commits into
mainfrom
feat/sandboxed-plugin-redesign

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic commented May 16, 2026

What does this PR do?

Reworks how sandboxed plugins are authored, built, and distributed. Three threads, one PR because they all touch the same plugin surface and changesetting them together is the only way to publish a coherent release.

  1. New authoring shape. Sandboxed plugins now export default { hooks, routes } satisfies SandboxedPlugin from emdash/plugin. No more definePlugin wrapper, no more runtime emdash import in plugin source. The new strict mapped SandboxedPlugin type gives full per-hook event + return type inference automatically. definePlugin is now native-only (throws with a pointer to the new shape if called without id).

  2. emdash-plugin build + emdash-plugin dev. The CLI's build pipeline is consolidated under packages/plugin-cli/src/build/. build reads emdash-plugin.jsonc + src/plugin.ts + optional package.json and emits dist/plugin.mjs (runtime bytes), dist/manifest.json (wire-shape PluginManifest with hooks/routes harvested from probing), and dist/index.mjs (bare descriptor module). dev is a chokidar-based watcher around the same pipeline with serialised rebuilds, last-good-on-failure semantics, and clean Ctrl-C drain. bundle is now a thin packaging step on top of build (rename plugin.mjs → backend.js inside the tarball only). Optional manifest version is reconciled with package.json#version (resolvePluginVersion).

  3. Package rename. @emdash-cms/registry-cli@emdash-cms/plugin-cli; binary emdash-registryemdash-plugin. The package's job has outgrown the original name — init, build, dev, bundle, publish, plus discovery + identity. The old package is no longer published.

All 5 in-tree plugins migrated to the new shape. Schema regenerated. 4 changesets in .changeset/.

Closes: see Discussion #296

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: Marketplace Discussion #296

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.7 (opencode)

Notes

Breaking changes

  • @emdash-cms/registry-cli is renamed to @emdash-cms/plugin-cli and the binary emdash-registry is now emdash-plugin. Old name stops getting published; users update their devDeps and scripts.
  • @emdash-cms/plugin-audit-log, @emdash-cms/plugin-webhook-notifier, @emdash-cms/plugin-atproto drop their named exports and factory call shape. Sites using them switch from import { auditLogPlugin } from "..."; plugins: [auditLogPlugin()] to import auditLog from "..."; plugins: [auditLog]. Per-plugin changesets document the migration.
  • emdash removes the definePlugin standard-format overload and the StandardPluginDefinition / isStandardPluginDefinition exports. Sandboxed plugins move to satisfies SandboxedPlugin from emdash/plugin. The runtime-side handle type formerly called SandboxedPlugin is renamed to SandboxedPluginInstance to free the name for the new author-facing type — relevant for anyone implementing a custom SandboxRunner (e.g. @emdash-cms/cloudflare was updated in this PR).

Authoring shape

+ import type { SandboxedPlugin } from "emdash/plugin";
- import { definePlugin, type ContentHookEvent, type PluginContext } from "emdash";

- export default definePlugin({
+ export default {
 	hooks: {
 		"content:beforeSave": {
-			handler: async (event: ContentHookEvent, ctx: PluginContext) => {
+			handler: async (event, ctx) => {
 				// ...
 				return event.content;
 			},
 		},
 	},
- });
+ } satisfies SandboxedPlugin;

Plugin package.json scripts

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

Adversarial review

Four rounds (12 → 4 → 2 → 0 substantive findings). Notable fixes from review:

  • init scaffold corrected to emit the new shape (was emitting the now-broken definePlugin template)
  • publish reads package.json#version and reconciles via normaliseManifest (was failing for the recommended manifest-version-omitted pattern)
  • dev watcher serialises rebuilds, drains Ctrl-C cleanly, handles Windows path separators
  • adapter normalises in-process Request to the strict SandboxedRequest shape so handlers behave identically in-process and in-isolate
  • build's readPackageMeta aligned with publish's strictness on malformed package.json#version

Verification

  • 3249 core tests pass
  • 255 plugin-cli tests pass
  • All 5 in-tree plugins build cleanly via emdash-plugin build
  • Typecheck clean across emdash, plugin-cli, cloudflare, all 5 plugins
  • Lint: 7 pre-existing warnings in files this PR does not touch, zero new

ascorbic added 7 commits May 14, 2026 15:09
…ract

First phase of the sandboxed plugin redesign (#1028b). Adds the
manifest fields that make `src/index.ts` and the in-code descriptor
factory redundant. The trust contract is now hand-authored in the
manifest, where a security reviewer can find it without grep.

New required fields:

- `slug`: ASCII letter then letters/digits/hyphens/underscores, max 64
  chars. Matches the registry lexicon's rkey grammar via the shared
  PLUGIN_SLUG_RE in @emdash-cms/plugin-types.
- `version`: semver 2.0 subset, no build-metadata (atproto rkeys can't
  contain `+`). Validated via PLUGIN_VERSION_RE.
- `publisher`: now required (was optional in #1028a). The runtime
  cannot compute the plugin's AT URI without it; making it optional
  meant the plugin couldn't load locally before first publish.

New optional fields with sensible defaults:

- `capabilities`: array of capability strings. Defaults to []. Each
  entry validated against the current vocabulary; deprecated names are
  hard-rejected with a hint at the replacement (no deprecation window
  for new authoring).
- `allowedHosts`: array of host patterns. Defaults to []. Required
  non-empty when `network:request` is declared without
  `:unrestricted`. Forbidden when `:unrestricted` is declared.
- `storage`: map of collection name -> { indexes, uniqueIndexes? }.
  Defaults to {}.

The cross-field rule for network:request / allowedHosts mirrors the
release-extension lexicon's networkRequestConstraints behaviour, so
authors hit the schema error here rather than a PDS validation error
at publish time.

Schema regenerated. 33 new tests; 204 total passing.

Part of #1028b. The bundle rewrite, init command, plugin migrations,
and `localPlugin` dev helper land in subsequent commits.
Second phase of the sandboxed plugin redesign (#1028b). Bundle no longer
imports src/index.ts for a descriptor factory; the manifest is the
source of truth for identity (slug, version) and the trust contract
(capabilities, allowedHosts, storage). Bundle still probes the runtime
code for the hook/route surface — that's a syntactic property that
needs the code to exist.

Changes to bundle:

- Drop the main-entry build and descriptor extraction. No more
  src/index.ts probing, no more `createPlugin` / default-factory /
  default-object format detection.
- Replace `resolveEntries`: just locates emdash-plugin.jsonc (loaded
  through the same loader the CLI's validate uses) and confirms
  src/plugin.ts exists. No more package.json `exports` parsing.
- Replace `extractResolvedPlugin` with `assembleResolvedPlugin`: builds
  the ResolvedPlugin shape from the manifest, then probes
  src/plugin.ts for hook/route names.
- Probe (renamed from `augmentWithSandboxProbe` to `probePluginSurface`)
  now reads src/plugin.ts. Hard-fails if the default export isn't a
  definePlugin result.
- New error codes: MISSING_MANIFEST, MISSING_PLUGIN_ENTRY,
  MANIFEST_INVALID. Old MISSING_PACKAGE_JSON / MISSING_ENTRYPOINT /
  MAIN_BUILD_FAILED gone.
- Admin entry handling (admin.js, adminPages, adminWidgets) deferred
  to a follow-up issue. The redesign hasn't touched admin yet; that
  surface stays as-is and is gated on the descriptor's `admin` field
  which no longer exists. When admin lands again it'll be a manifest
  field with its own probe.

Changes to translate.ts:

- `NormalisedManifest` gains slug, version, publisher (required),
  capabilities, allowedHosts, storage. Publisher is no longer
  Optional — the schema enforces it.

Fixtures:

- `minimal-plugin/`: src/index.ts gone, sandbox-entry.ts renamed to
  plugin.ts, new emdash-plugin.jsonc with identity + trust contract.
- `bad-plugin/`: stripped to manifest-only (no src/), exercises
  MISSING_PLUGIN_ENTRY. Old "declares hooks but no sandbox entry"
  case isn't possible anymore — there's no descriptor declaring
  anything.

Net diff: -228 lines.
Third phase of the redesign (#1028b). Adds `emdash-registry init [name]`
which produces the three-file plugin layout introduced by the previous
commits: emdash-plugin.jsonc, src/plugin.ts, package.json, plus a
tsconfig, README, .gitignore, and a passing test.

Modes:
- Interactive (default on a TTY): clack prompts for each unset field
  with sensible defaults. ESC / Ctrl+C cancels cleanly.
- `--yes` / `-y` (non-interactive): no prompts; unset fields become
  TODO placeholders in the manifest. The author fixes them before
  first use.
- Non-TTY (CI, pipes): same as `--yes`; prompting into a non-
  interactive stdin would hang.

Pre-fills:
- Publisher: the active session's handle from FileCredentialStore.
  Resolved through @atcute/identity-resolver to a DID before write
  so the runtime never sees a mutable handle. The handle is emitted
  as a `// <handle>` line comment next to the pinned DID for `git
  diff` readability — same convention as the post-publish write-back.
- Author name / email: `git config user.name` / `user.email`.
- Repo: `git remote get-url origin`, normalised from SSH to https
  (`git@github.com:foo/bar.git` → `https://github.com/foo/bar`).
  Falls back to `package.json#repository.url` if no git remote.
- License, description: `package.json` in the target dir if one
  exists (for the "scaffold into existing repo skeleton" case).

Slug defaults to the positional `name`, `basename(--dir)`, or
basename(cwd) in that order. Every flag is optional in every mode.

Exported `resolveHandleToDid` from manifest/publisher.ts so init
can use the same resolver the post-publish write-back does.

Tests: 44 new (template renderers, scaffold filesystem behaviour,
environment probe). 249 total in the package.
Fourth phase of the redesign (#1028b). Moves the 5 in-tree sandboxed
plugins to the manifest + src/plugin.ts shape so they become the
canonical references a plugin author looks at.

Each plugin's layout changes from:

  src/index.ts          (descriptor factory, ~50 lines)
  src/sandbox-entry.ts  (runtime code via definePlugin)
  package.json          (main / exports / files / build scripts)

to:

  emdash-plugin.jsonc   (identity + trust contract + admin surface)
  src/plugin.ts         (runtime code, unchanged)
  package.json          (private, typecheck script only)

Plugins migrated:
- atproto
- audit-log
- marketplace-test
- sandboxed-test
- webhook-notifier

Schema gains `admin` (pages + widgets) since four of the five plugins
declare admin surface. Mirrors PluginAdminPage / PluginDashboardWidget
in core. Atproto's plugin.test.ts rewritten to assert against the
manifest instead of the deleted descriptor factory.

KNOWN BREAKAGE: demos that import the old factories
(`auditLogPlugin()`, `webhookNotifierPlugin()`) from
astro.config.mjs are broken until the next commit ships
`@emdash-cms/registry-cli/dev`'s `localPlugin(dir)` helper and
updates the demos.

All published plugins still work — the bundled manifest.json shape
is unchanged. Only authoring changed.
Final piece of the sandboxed-plugin redesign (#1028b). Closes the gap
the plugin migrations opened — demos that previously imported
`auditLogPlugin()` / `webhookNotifierPlugin()` factories now consume
the plugins through their source directories.

New subpath `@emdash-cms/registry-cli/dev` exports `localPlugin(dir)`,
which:

- Reads `<dir>/emdash-plugin.jsonc` via the same loader the CLI uses.
- Confirms `<dir>/src/plugin.ts` exists.
- Resolves the manifest's publisher (handle → DID) so the descriptor
  is in canonical form.
- Returns a PluginDescriptor-shaped object with `entrypoint` set to
  the absolute `file://` URL of `src/plugin.ts`. Vite resolves the
  URL through its standard fs path resolver — no build step needed.

The descriptor carries id, version, capabilities, allowedHosts,
storage, and (when declared) adminPages + adminWidgets from the
manifest. Plugins that don't expose admin surface pass through
without the optional fields, keeping the descriptor tidy.

Demos updated:
- demos/simple: auditLogPlugin() → localPlugin("../../packages/plugins/audit-log")
- demos/plugins-demo: auditLog + webhookNotifier the same way
- demos/cloudflare: webhookNotifier via localPlugin
- infra/cache-demo, infra/blog-demo: same

Trusted plugins (formsPlugin, embedsPlugin, apiTestPlugin) keep their
factory-based imports — they're not on the new shape and aren't part
of this redesign's scope.

Errors surface as a structured LocalPluginError with codes:
- MANIFEST_INVALID
- PLUGIN_ENTRY_MISSING
- PUBLISHER_UNRESOLVED

Tests: 10 new (descriptor shape, error paths, admin pass-through).
259 total in the package.
Renames @emdash-cms/registry-cli to @emdash-cms/plugin-cli and the
binary emdash-registry to emdash-plugin. Adds build + dev commands,
consolidates the build pipeline so bundle is a thin packaging step on
top of build. Introduces a strict author-facing SandboxedPlugin type
via the new emdash/plugin type-only subpath; sandboxed plugins now
default-export a bare { hooks?, routes? } object with satisfies
SandboxedPlugin and have no runtime emdash import. Drops definePlugin
and the build shim for sandboxed plugins (definePlugin is native-only
now). Migrates the five in-tree sandboxed plugins to the new shape.
Manifest version is optional and reconciled with package.json#version.
- init scaffold emits the new `satisfies SandboxedPlugin` shape and
  npm-shape package.json (build/dev scripts, ./sandbox export, plugin-cli
  devDep) instead of the broken `definePlugin` template
- publish reads package.json#version and reconciles via normaliseManifest
  so the new "version in package.json only" pattern actually publishes;
  malformed package.json surfaces a CliError, not a misleading
  VERSION_MISSING further down
- dev watcher serialises rebuilds (queue collapsed to one follow-up),
  closes the watcher before draining pending on Ctrl-C, short-circuits
  scheduleRebuild during shutdown, handles Windows path separators in
  the outDir ignore glob, clears pending+queuedTrigger in finally so an
  IIFE rejection can't deadlock the session, and removes SIGINT handlers
  on shutdown
- adapter normalises ctx.request to SandboxedRequest shape in-process
  so handlers see the same { url, method, headers: Record } promised by
  the strict type; null/array/non-object default exports rejected with
  a plugin-id-bearing message
- build's readPackageMeta rejects empty/non-string version with the
  same strictness as publish, killing the build-pass/publish-fail
  asymmetry
- pipeline probe rejects invalid hook config (errorPolicy, priority,
  timeout) so untyped JS authors get a build error rather than a
  silently-wrong runtime contract
- versionless minimal-plugin fixture so bundle/publish/build integration
  tests exercise the package.json-as-source-of-truth path
- definePlugin error wording softened for native-plugin authors whose
  id field has a typo
- pipeline error messages and stale comments updated for the no-shim,
  no-definePlugin authoring shape
- removed dead EMDASH_SHIM from the Cloudflare sandbox runner
- changesets retargeted to @emdash-cms/plugin-cli; scaffold/atproto/core
  comments scrubbed for stale registry-cli references
Copilot AI review requested due to automatic review settings May 16, 2026 06:31
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 16, 2026

🦋 Changeset detected

Latest commit: 7ffc93c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@emdash-cms/plugin-cli Minor
emdash Minor
@emdash-cms/plugin-atproto Minor
@emdash-cms/plugin-audit-log Minor
@emdash-cms/plugin-webhook-notifier Minor
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/cloudflare Minor
@emdash-cms/fixture-perf-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 16, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 7ffc93c May 18 2026, 11:29 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 16, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-i18n 7ffc93c May 18 2026, 11:28 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 16, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 7ffc93c May 18 2026, 11:29 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 16, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-perf-coordinator 7ffc93c May 18 2026, 11:28 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 16, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 7ffc93c May 18 2026, 11:30 AM

@github-actions
Copy link
Copy Markdown
Contributor

Scope check

This PR changes 6,993 lines across 100 files. Large PRs are harder to review and more likely to be closed without review.
This PR spans 5 different areas (area/core, area/admin, area/plugins, area/docs, area/cloudflare). Consider breaking it into smaller, focused PRs.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Renames @emdash-cms/registry-cli@emdash-cms/plugin-cli (binary emdash-registryemdash-plugin), introduces a new sandboxed plugin authoring shape (satisfies SandboxedPlugin from emdash/plugin), and consolidates the plugin build pipeline under a new build/dev/bundle CLI surface. The visible diffs in this slice are mostly downstream updates: in-tree templates and demo astro.config.mjs files switch from factory imports (auditLogPlugin(), webhookNotifierPlugin()) to default-export instances, agent-skill docs update their snippets to match, the workspace catalog adds chokidar and drops the Zod pin comment, and several files in packages/registry-client plus the now-deleted packages/registry-cli fixtures get their textual emdash-registry references retargeted to emdash-plugin.

Changes:

  • Template + demo configs and agent-skill documentation updated for the new default-export plugin shape.
  • packages/registry-client doc/comment/error-message references updated from emdash-registry / @emdash-cms/registry-cli to emdash-plugin / @emdash-cms/plugin-cli.
  • pnpm-workspace.yaml catalog adds chokidar ^5.0.0 (used by the new dev watcher), re-sorts jsonc-parser, and removes the Zod-pin rationale comment (pin itself remains).

Reviewed changes

Copilot reviewed 125 out of 147 changed files in this pull request and generated no comments.

Show a summary per file
File Description
templates/*/.agents/skills/building-emdash-site/references/configuration.md (×8) Update docs snippet from auditLogPlugin() factory to auditLog default export.
templates/blog/astro.config.mjs Switch audit-log plugin to default-export usage.
templates/blog-cloudflare/astro.config.mjs Switch webhook-notifier sandboxed plugin to default-export usage.
pnpm-workspace.yaml Add chokidar ^5.0.0, reorder jsonc-parser, drop Zod-pin comment.
packages/registry-client/src/index.ts, publishing/index.ts, credentials/types.ts, credentials/file.ts, README.md Rename CLI references from emdash-registry/@emdash-cms/registry-cli to emdash-plugin/@emdash-cms/plugin-cli.
packages/registry-cli/tests/fixtures/{minimal-plugin,bad-plugin}/src/*.ts Delete obsolete fixture files (registry-cli package removed).
packages/registry-cli/src/manifest/translate.ts Delete obsolete translate module.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions
Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

"INVALID_PLUGIN_FORMAT",
`${entries.pluginEntry} must default-export an object with \`hooks\` and/or \`routes\` (sandboxed plugin shape: \`export default { hooks, routes } satisfies SandboxedPlugin\` from "emdash/plugin"). Got ${describeShape(definition)}.`,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BUG: Missing default export silently produces an empty plugin

Category: Error Handling / Data Integrity
Severity: HIGH

(pluginModule.default ?? {}) substitutes an empty object when default is undefined. The shape check on the next line (typeof === "object" && !null && !Array.isArray) passes for {}, so the INVALID_PLUGIN_FORMAT guard never fires. hooks and routes then read as undefined and the loops skip silently. buildPlugin returns success, writes dist/manifest.json with empty hooks: [] / routes: [], and emits a runtime plugin.mjs. The author sees a green build for a plugin that does nothing.

Trigger: write src/plugin.ts as export const plugin = { ... } satisfies SandboxedPlugin (forgot default), or export { plugin as default } satisfies ... mistyped, or any source where the bundler tree-shakes the default away. pnpm build succeeds; the bundle ships and no hook ever fires.

Note the error message at line 303 already says "must default-export an object" — the check just doesn't run for the missing-default case.

Fix: detect the missing default explicitly before the falsy coalesce, e.g.

if (!("default" in pluginModule)) {
  throw new BuildPipelineError(
    "INVALID_PLUGIN_FORMAT",
    `${entries.pluginEntry} has no \`default\` export. ...`,
  );
}
const definition = pluginModule.default;

or fold the missing case into the existing INVALID_PLUGIN_FORMAT branch by checking pluginModule.default === undefined first.

Comment thread packages/plugin-cli/src/init/templates.ts Outdated
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 16, 2026

Review posted successfully. Summary of what I posted to PR #1057:

Two findings, line-anchored:

  1. HIGH — packages/plugin-cli/src/build/pipeline.ts:305 — Missing default export silently produces an empty plugin. (pluginModule.default ?? {}) falls back to {} which passes the INVALID_PLUGIN_FORMAT shape check, leaving hooks and routes undefined; build succeeds with zero behavior and ships a manifest with empty hooks: [] / routes: [].

  2. MEDIUM — packages/plugin-cli/src/init/templates.ts:307 — Scaffolded README emits invalid JavaScript when the slug contains a hyphen (the schema's canonical "my-plugin" example). Template uses import ${slug} from ... directly, producing import my-plugin from "..." which is a syntax error. The init-templates.test.ts suite only exercises the single-word slug "gallery", so this isn't caught by tests.

github run

ascorbic and others added 3 commits May 16, 2026 09:03
- Delete `packages/marketplace/tests/publish-e2e.test.ts` — invoked the
  legacy `emdash plugin bundle` from core CLI against the new
  manifest-driven plugin layout, which it doesn't understand.
- Remove the validate-plugins CI job — it used the same legacy CLI
  command. Plugin validation is now covered by `pnpm build`, which
  runs the new `emdash-plugin build` probe + manifest checks against
  every in-tree sandboxed plugin.
- Fix `no-base-to-string` lint errors in audit-log/plugin.ts. The
  canonical ContentHookEvent types `event.content.id` as unknown;
  `String(unknown)` lands on '[object Object]' for record IDs. Added
  a small `stringifyId` helper that returns '' for non-string/number
  inputs so the caller's existence check skips bad rows.
- pipeline.ts now hard-errors when the probed module has no `default`
  export, instead of silently falling through to an empty plugin
  (build had been writing dist/ artifacts with empty hooks/routes for
  any source that used `export const plugin = ...`).
- Scaffold README camelCases hyphenated slugs for the import binding.
  Slugs like `my-plugin` were producing `import my-plugin from ...`
  which is a syntax error. Test added with a hyphenated fixture.

Both bot review comments addressed.
bundle.test.ts > 'produces a tarball + manifest for a minimal valid
plugin' timed out at the 5s default on the GitHub-hosted runner.
The test runs the full build pipeline (tsdown probe + transpile +
tarball pack), which is fast locally (<2s) but cold-starts at 5-8s
on CI. Bump to 30s globally for the plugin-cli vitest config.
@ascorbic
Copy link
Copy Markdown
Collaborator Author

@ask-bonk merge main and update the lockfile

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 18, 2026

To https://github.com/emdash-cms/emdash.git
! [remote rejected] feat/sandboxed-plugin-redesign -> feat/sandboxed-plugin-redesign (refusing to allow a GitHub App to create or update workflow .github/workflows/ci.yml without workflows permission)
error: failed to push some refs to 'https://github.com/emdash-cms/emdash.git'

github run

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 18, 2026

@ascorbic Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@ascorbic
Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: ascorbic <213306+ascorbic@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 18, 2026

@copilot resolve the merge conflicts in this pull request

Resolved and pushed in bfacc1c. This merges origin/main, clears the conflict, and updates the merged lockfile.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • pkg.pr.new
    • Triggering command: /home/REDACTED/.npm/_npx/52027bd8fc0022aa/node_modules/.bin/node node /home/REDACTED/.local/share/pnpm/.tools/pnpm/11.1.2/bin/pnpm install --lockfile-only --no-frozen-lockfile d -n 10 (dns block)
    • Triggering command: /home/REDACTED/.npm/_npx/52027bd8fc0022aa/node_modules/.bin/node node /tmp/pnpm-10.28.0/package/bin/pnpm.cjs install --lockfile-only --offline --ignore-scripts (dns block)
    • Triggering command: /home/REDACTED/.npm/_npx/52027bd8fc0022aa/node_modules/.bin/node node /tmp/pnpm-10.28.0/package/bin/pnpm.cjs install --lockfile-only --offline --ignore-scripts --global p/bin/git (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@ascorbic
Copy link
Copy Markdown
Collaborator Author

@ask-bonk kimi update the lockfile

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 18, 2026

Done. The lockfile has been regenerated to account for the @clack/prompts@^1.4.0 dependency added to packages/plugin-cli/package.json (and the broader workspace rename from @emdash-cms/registry-cli@emdash-cms/plugin-cli). Committed as 7ffc93c on top of the merge commit.

github run

@github-actions
Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


1 out of 2 committers have signed the CLA.
✅ (ascorbic)[https://github.com/ascorbic]
❌ @Copilot
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@ascorbic ascorbic changed the title feat(plugin-cli): sandboxed plugin authoring + build + dev (with rename) feat(plugin-cli): sandboxed plugin authoring CLI May 18, 2026
@ascorbic ascorbic merged commit c0ce915 into main May 18, 2026
37 of 38 checks passed
@ascorbic ascorbic deleted the feat/sandboxed-plugin-redesign branch May 18, 2026 14:01
ascorbic added a commit that referenced this pull request May 18, 2026
* docs(plugins): document the plugin CLI, manifest, and authoring-shape changes (#1040, #1057)

Adds migration guides for site operators and plugin authors, new
reference pages for emdash-plugin.jsonc and the emdash-plugin CLI, and
rewrites the sandboxed-plugin guides to the new default-export shape.
Migration guides follow Astro's breaking-changes format; Atmosphere
account terminology used throughout.

* docs(plugins): apply definition-by-negation / builder-salience standard

Self-review of #1059 against the standards formalized since it was written.
Removed definition-by-negation and bundle-internal framing from the guide
pages (migration guides left as-is — comparison/changelog is their purpose):

- 'You do not write a descriptor or a build script' -> dropped (the positive
  sentence already says what build does)
- 'the build produces both, so you never hand-write either' -> 'generates both'
- 'type-only, so the emdash runtime does not enter the plugin bundle' /
  'the bundler erases them — no emdash runtime enters' -> 'provides only
  types, so a sandboxed plugin has no runtime dependency on emdash'
- 'you never write it by hand' -> 'EmDash derives ... automatically'
- 'init never requires extra flags to succeed' -> 'A slug is the only
  required input'
- 'wire-side filename' -> '(the filename the registry expects)'
- 'The registry never stores your plugin's code' lead -> dropped; the
  positive 'you host the tarball; registry stores a link' carries it

Tropes scan clean; em-dashes appositive; bold bullets definitional.

* docs(plugins): address review feedback on #1059

- Manifest profile prose lists author and security contact as required,
  matching the table and the actual ManifestSchema (.refine() rules in
  packages/plugin-cli/src/manifest/schema.ts require one of author/
  authors and one of security/securityContacts).
- Publishing prerequisites match the same required-fields story.
- Bundle flag documentation uses kebab-case (--out-dir, --validate-only)
  to match the post-rename CLI (#1091).
- your-first-plugin scaffolding example bumps emdash to >=0.13.0 (the
  first release exposing the emdash/plugin entrypoint) and pins
  @emdash-cms/plugin-cli to 0.2.0 (exact pin per the publishing page's
  experimental-registry guidance).

* docs(plugins): address second round of review feedback on #1059

- api-routes: clarify that the route URL segment is the plugin's slug
  (the same value as ctx.plugin.id at runtime) — previously phrased as
  <plugin-id> without explaining what that is.
- api-routes: SandboxedRouteContext is non-generic in emdash/plugin
  (input is unknown; authors narrow with a route-level Zod schema). The
  earlier reference snippet showed a TInput generic that doesn't exist
  in the exported type.
- cli: --json is supported by the non-interactive output commands
  (whoami, validate, search, info, login, publish), not all commands —
  logout, switch, init, build, dev, bundle do not define a json arg.

* docs(plugins): align discovery flag and prose on registry terminology

The CLI flag for overriding the discovery endpoint is being renamed
from --aggregator to --registry-url in #1092, matching the
EMDASH_REGISTRY_URL env var and the user-facing concept of a registry.
Update the docs to use the new flag name and drop the user-facing
mention of 'aggregator' in publishing.mdx in favour of 'registry'.
@emdashbot emdashbot Bot mentioned this pull request May 18, 2026
ski added a commit to ski/emdash that referenced this pull request May 20, 2026
Catches deployment/suhail-ski up with emdash-cms/emdash main. Conflict in
pnpm-lock.yaml resolved by taking upstream's lock and re-running
pnpm install to pick up the @suhailski/57th-parallel template deps.

Notable upstream changes folded in:
  - Plugin authoring CLI + manifest format (emdash-cms#1057, emdash-cms#1059)
  - Registry packages — lexicons, client, CLI (emdash-cms#923)
  - Migration 036 taxonomy preservation (emdash-cms#1086)
  - WXR import w/ WPML/Polylang translations (emdash-cms#1087)
  - TypeScript 6 upgrade, tsgo beta (emdash-cms#1074)
  - FTS5 corruption fix on publish (emdash-cms#768)
  - Plugin bundle size caps (emdash-cms#978)
  - And ~280 smaller fixes / chores

CLI rebuilt against the new core. Verified by listing posts on
suhail.ski — both tutorial posts come back as expected.

57th-parallel template untouched by upstream. Our 5 local commits
preserved across the merge.

Used --config.manage-package-manager-versions=false to bypass pnpm's
self-switch to 11.1.3, which packages weirdly on Windows. The current
shell's pnpm 10.12.4 worked fine against the merged manifests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants