✨ Highlights

This release fixes a regression that broke asset loading on GitHub Pages project sites (#175), hardens the plugin/template/engine auto-install pipeline against shell injection (CWE-78), and removes @docmd/plugin-pwa from the default install (it was never a core plugin).

No breaking changes. No new required config. Existing configs, plugins, and CI pipelines work unchanged.

🐛 Fix: GitHub Pages project sites 404 on all assets (#175)

Users deploying to https://username.github.io/repo-name/ saw every CSS, JS, and image file 404 after upgrading from 0.8.9. The cause: 0.8.10 added a <base href="/"> tag to fix a separate workspace issue, which made the browser resolve all relative URLs against the domain root instead of the subpath.

The fix: docmd now derives the asset base path from your url config automatically. If url is https://username.github.io/my-repo, docmd sets base to /my-repo/ internally. No manual base config is needed.

docmd.config.json
{
  "url": "https://username.github.io/my-repo"
}

That’s it. docmd handles the rest.

Users who explicitly set base are unaffected. The derivation is slash-tolerant on both url and base"my-repo", "/my-repo", and "/my-repo/" all work.

🔒 Security: shell injection in auto-install (CWE-78)

The plugin auto-install path built a shell-string command by interpolating the package name into pnpm add ${pkg}. A package name with shell metacharacters that ever reached this code path would execute arbitrary commands under the user’s account.

The fix: a new shared module (packages/api/src/runtime-deps.ts) replaces the shell-string with spawn(pm, args, { shell: false }) using a fixed arg array. A strict regex validator (/^@docmd\/(plugin|template|engine)-[a-z0-9][a-z0-9.-]*$/) and a registry lookup act as defence-in-depth. The auto-install path can no longer be turned into a generic npm install loader.

🧰 Shared runtime-deps module

The plugin loader (hooks.ts) and the engine loader (engine.ts) each used to carry their own copy of the auto-install pipeline. Both now go through runtime-deps.ts. One install behaviour, applied everywhere. The TUI status reporter is idempotent per-build, so dev-server rebuilds do not spam duplicate [ WAIT ] / [ DONE ] lines.

🛠 Engine auto-install

Requesting engine: "rust" in config used to fall straight through to JS if @docmd/engine-rust was not installed. The new flow tries to install engine-rust first; if the install fails or the native binary is not usable on the platform, it falls back to JS and surfaces the reason in a [ FAIL ] line. The same applies to loadEngine('js') as a last resort.

📦 Corrected core dependency manifest

@docmd/plugin-pwa was listed as a direct dependency of @docmd/core but was never in CORE_PLUGINS (the source-of-truth constant that drives auto-loading). Every npm install @docmd/core pulled it in, even for users who never enabled PWA.

The fix: removed @docmd/plugin-pwa from packages/core/package.json#dependencies. It now installs on demand, the same as math, threads, and every other optional plugin. The dependency list now matches CORE_PLUGINS exactly.

📐 Test coverage

51 new assertions in tests/cli-contracts/runtime-deps.test.js: public surface, validator (12 positive, 13 negative cases including shell metacharacters), registry cache idempotency, install refusal paths, and static source checks proving no execSync shell-string remains in hooks.ts or engine.ts.

Migration

Zero. Existing configs, plugins, and CI pipelines work unchanged.

Full changelog: https://github.com/docmd-io/docmd/compare/0.8.11...0.8.12