docmd 0.8.9 π Plugin + Semantic 0.1.0
Build production-ready documentation from Markdown in seconds.
β¨ Highlights
This release is a hardening pass on the plugin and template ecosystem. The biggest change is structural: every official @docmd/* package now carries a docmd namespace in its package.json, and a build-time registry generator reads those namespaces to produce the single source of truth that the runtime loader consumes. The plugin auto-installer is now resilient to packages that ship import-only exports fields, and a new docmd doctor pre-flight command catches configuration drift before a build. Two previously-silent bugs in published tarballs are also fixed: the 404 page now renders with the summer template and translated strings instead of falling back to the default template with raw translation keys.
No public API changes. No breaking config changes. Purely a hardening release.
π Build-time plugin registry (single source of truth)
A new workspace-level script β scripts/build-plugin-registry.mjs β walks packages/{plugins,templates,engines}/*, reads each package's package.json#docmd namespace, and emits a generated JSON catalog (packages/api/registry/plugins.generated.json). Wired as the prebuild step of @docmd/api, so the registry is regenerated on every build.
The runtime loader (packages/api/src/hooks.ts getPluginRegistry) now reads from the generated file, with two resolution paths (published layout <pkg>/registry/... and monorepo dev layout <repo>/packages/api/registry/...). The hand-maintained packages/plugins/installer/registry/plugins.json that lived in the installer is no longer consulted at runtime.
π¦ docmd namespace standardisation
Every official @docmd/* package now carries a docmd namespace in its package.json. The namespace is the contract that the registry generator reads and the loader cross-checks against the JS descriptor at load time. Fields: key (user-facing identifier), kind (plugin / template / engine), displayName, tagline, capabilities (required for plugins and templates, used by the build-time cross-check), and preview (template only).
Engines get the same shape but no capabilities β they don't participate in the hook system. The auto-installer checks kind === 'engine' and refuses to install them.
π©Ί docmd doctor β pre-flight check
A new CLI subcommand for diagnostics. No filesystem writes, no build side-effects β purely diagnostic.
npx @docmd/core doctor [options]| Option | Description |
|---|---|
--config <path> |
Path to a non-default docmd.config.json (or .ts/.js/.mjs). |
--fix |
Auto-install missing official plugins or templates. |
--json |
Emit the report as machine-readable JSON. |
By default, doctor prints a human-readable summary covering the installed @docmd/core version, every configured plugin (with version and β installed / β missing status), the active template, the requested engines (js always-on, rust opt-in), and a list of auto-install candidates. With --fix, it shells out to the project's package manager to install the candidates. With --json, the same data is emitted as a single JSON object β useful for pre-commit hooks and CI gates.
Also available as pnpm doctor (which routes through the workspace docmd script in the monorepo).
π New pnpm scripts in the monorepo
The monorepo's package.json gets a batch of new pnpm scripts, all pointing at the playground via the existing --cwd flag (same pattern as pnpm dev and pnpm live):
pnpm doctor # β docmd doctor
pnpm validate # β docmd validate
pnpm migrate # β docmd migrate
pnpm gen:deploy # β docmd deploy
pnpm mcp # β docmd mcp
pnpm plugin:add foo # β docmd add foo
pnpm plugin:remove foo # β docmd remove foo
pnpm build:playground # β docmd build (separate from `pnpm build` which is the monorepo-wide build)π Bug fixes
- Plugin auto-install: resilient to
import-onlyexports. The auto-installer in@docmd/apipreviously usedrequire.resolveto resolve installed packages, which throwsERR_PACKAGE_PATH_NOT_EXPORTEDfor packages that ship only animportcondition inexports. The retry path now usesawait import(name)directly, which honours theexportsfield natively. A defense-in-depth registry re-check is performed inside the retry path β the set of names that can be auto-installed is unchanged, only the set ofexportsconditions they can carry is larger. - Plugin loader: capability cache and manifest drift check. A per-key capability set cache avoids re-walking the registry on every dev-server rebuild. A new manifest drift check warns if the JS descriptor's
capabilitiesarray doesn't match the manifest'scapabilitiesarray β closing the silent-hook-drop bug where a plugin that implemented a hook but forgot the corresponding capability in its descriptor would have its hook silently skipped. - Better "Could not load X after auto-install" errors. The post-install retry's catch block now surfaces
err.code(e.g.ERR_PACKAGE_PATH_NOT_EXPORTED,ERR_MODULE_NOT_FOUND) and the first line oferr.message. TheautoInstallPlugincatch block also surfaces the underlying stderr from the package manager and prints a hint for the most common cases. @docmd/ui: includetranslations/in the published tarball. The server-side translation loader reads translation JSON files at__dirname/../translations/. Thepackage.json#fileswas missing"translations", so npm packed the tarball without the translation files. The 404 page was the most visible symptom: rendered lazily by the deployed site's static-file fallback, the translation cache was empty, and the keys leaked. Now all 7 locale files ship.@docmd/template-summer: includetemplates/andassets/in the published tarball. The summer template's runtime usesnew URL('../templates/...', import.meta.url)insidedist/index.js, which resolves to<package-root>/templates/...(not<package-root>/dist/templates/...). Thepackage.json#fileswas just["dist"], so the published tarball only containeddist/, and the resolver fell back to the default template. Now the published tarball matches the monorepo dev layout and the summer template renders correctly.- Dev server: strip leading slash before
safePath(). The CWE-22 fix from 0.8.9 (replacingfilePath.startsWith(rootAbs)withsafePath(rootAbs, ...)) had a regression in the dev server: URL pathnames always start with/, andpath.resolve('/abs/root', '/index.html')returns/index.htmlβ which always failed thesafePathboundary check and produced403 Forbiddenfor every legitimate request. Fix: strip the leading/from the URL pathname before passing tosafePath(). - Live editor, port probe, and Docker docs default to loopback. The live editor's
server.listenand thecheckPortInUseprobe now bind to127.0.0.1instead of0.0.0.0. LAN access is opt-in viaDOCMD_HOST=0.0.0.0or--host 0.0.0.0, with a TUI warning when active. Thedocker/DOCKER.mdexamples now use the loopback default. - Utils:
scriptLiteralandjsonInjectharden inline script escaping. Both now properly escape</script,<!--, U+2028, and U+2029 in addition to the existing JSON-safe encoding. The hardening is silent for non-conflicting strings; round-trips throughJSON.parsestill work because the escape uses JSON-safe sequences.
Changelog
- Registry: New
scripts/build-plugin-registry.mjsβ generatespackages/api/registry/plugins.generated.jsonfrom each official package'sdocmdnamespace. - Loader:
getPluginRegistryreads the generated file with two resolution paths; the hand-maintainedinstaller/registry/plugins.jsonis no longer consulted at runtime. - Namespace:
docmdnamespace added to all 14 official@docmd/*packages (12 plugins, 2 engines) and updated on@docmd/template-summerwithkey: "summer". - Loader: Per-key capability set cache + manifest drift check (
checkManifestCapabilityDrift). - Auto-install: Retry path uses
await import(name)instead ofrequire.resolve + import(file://path), with a defense-in-depth registry re-check. - Auto-install errors:
err.code+ first line oferr.messagesurfaced; package-manager stderr + hint printed. - CLI:
docmd doctorsubcommand added with--config,--fix,--jsonflags. Registered in the CLI dispatcher; help text updated. - CLI: New pnpm scripts in the monorepo β
doctor,validate,migrate,gen:deploy,mcp,plugin:add,plugin:remove,build:playground. - UI:
packages/ui/package.json#filesnow includestranslations/. - Template:
packages/templates/summer/package.json#filesnow includestemplates/andassets/. - Dev server:
safePath()inserveStaticstrips the leading/from URL pathnames. - Live editor:
server.listenandcheckPortInUsebind to127.0.0.1;DOCMD_HOST=0.0.0.0opt-in. - Docker docs: Three
command: dev --host 0.0.0.0examples replaced with the loopback default; "Network Issues" troubleshooting documents the opt-in path. - Utils:
scriptLiteralandjsonInjectescape</script,<!--, U+2028, U+2029. - Tests:
packages/utils/test/html-escape.test.jsupdated to assert the new escape behaviour. - Docs: New "ESM Exports β the
defaultCondition" section inbuilding-plugins.mdandbuilding-templates.md; new "Bundled registry removal in 0.9.0" callout; newdocmd doctorentry inreference/cli-commands.md.
Thanks π
Thanks to all contributors and community members who reported issues and provided feedback.
Documentation: https://docs.docmd.io/
GitHub: https://github.com/docmd-io/docmd
Semantic Search (docmd-search): https://github.com/docmd-io/docmd-search
Skills: https://github.com/docmd-io/docmd-skills
Full Changelog: 0.8.8...0.8.9
