A focused hardening release.
No new features — just fixes for the rough edges reported since 0.8.7 shipped.
Every change here is either a regression caught by users, a CI workflow bug, or a build-time reliability issue.
🐛 Bug fixes
Docker: volume mount permissions fixed
docmd init and other file-writing commands failed with “Permission denied” when /docs was a host-mounted volume owned by a uid other than the container’s built-in docmd user (uid 1001).
The image now ships with su-exec and the entrypoint detects the host uid:gid of the mounted directory and re-execs as that identity before running any command — same pattern used by the official postgres and redis images.
# All of these now work without -u flags on a host volume
docker run -v $(pwd):/docs ghcr.io/docmd-io/docmd:latest init
docker run -v $(pwd):/docs -p 3000:3000 ghcr.io/docmd-io/docmd:latest
The cp -a (archive) mode that triggered “Operation not permitted” warnings on ownership preservation is gone.
The -u $(id -u):$(id -g) workaround is no longer required and has been removed from all READMEs and the Docker deployment guide.
Docker: init now non-blocking in CI
The interactive “Do you want to override these files?” prompt hung in non-TTY environments (CI, docker run without -it, piped input).
The CLI now detects non-interactive mode and skips the prompt automatically — keeping existing files by default (safe), or accepting -y/--yes to opt into overwrite.
A new --force flag also works in any environment for explicit override.
# Force overwrite in CI / Docker / piped input
docker run --rm -v $(pwd):/docs ghcr.io/docmd:latest init --force
# Or use -y
docker run --rm -v $(pwd):/docs ghcr.io/docmd:latest init -y
docmd add on npx: false “npm not found” on Windows
npx @docmd/core add <plugin> on Windows PowerShell printed “The package manager ‘npm’ was not found on your system PATH” even though npm --version worked fine in the same shell.
Root cause: when @docmd/core is launched through npx, the spawned child process inherits a stripped PATH on Windows.
The installer now resolves npm/pnpm/yarn/bun relative to process.execPath (Node’s own location), which always works regardless of how @docmd/core was launched.
The error message also now detects the specific case (Node reachable + binary missing) and suggests installing the plugin via the host package manager directly.
Template registry lookup for @docmd/template-*
Setting theme.template to an official template like summer or @docmd/template-summer printed:
⬢ Plugin "template-summer" not found in official registry - manual installation required
Root cause: the auto-install registry lookup stripped @docmd/plugin- from the package name to derive the registry key, but didn’t strip @docmd/template-.
So @docmd/template-summer resolved to key template-summer instead of summer, which doesn’t exist in the registry.
Now both prefixes are stripped correctly.
Custom/third-party templates were never affected.
Semantic search peer dependency handling
When plugins.search.semantic = true was set and the ML peer dependencies (@huggingface/transformers, onnxruntime-node) weren’t present, the build silently fell back to keyword search with a confusing error buried deep inside docmd-search:
⬢ Semantic indexing failed: Embedding failed: Failed to load model "Xenova/all-MiniLM-L6-v2": Cannot find package '@huggingface/transformers'
Two improvements:
- Clear pre-flight check — the plugin now verifies the peer dependencies are resolvable after auto-install and emits an actionable error naming the missing package, instead of letting the failure surface from inside
docmd-search. - Stale module cache fix — after auto-installing
docmd-searchmid-build, the current Node process’s module cache couldn’t see the new packages.
Indexing now runs in a child process so it starts with a clean module resolution.
npm installauto-install also passes--foreground-scriptssoonnxruntime-node’s postinstall (native binary download) actually runs in CI environments with npm’sallow-scriptssecurity feature enabled.
npm-publish: packages/templates/* now in the publish loop
The npm-publish.yml workflow was iterating packages/*, packages/legacy/*, packages/plugins/*, packages/engines/* but missing packages/templates/*.
Templates like @docmd/template-summer and @docmd/template-rain were not being released.
Now included.
docmd init no longer makes a network call by default
Previously docmd init always fetched SKILL.md from raw.githubusercontent.com/docmd-io/docmd-skills on every run.
The fetched content overrode the bundled default if it succeeded.
Two changes:
- Default is now local-only — the bundled
SKILL.mdcontent ships with the package and is written without any network call. - Opt in via
--with-skillflag orDOCMD_FETCH_REMOTE_SKILL=1env var to fetch the latest SKILL.md from the docmd-skills repo (CI environments and offline workstations can keep using the bundled default).
docmd init --with-skill # fetch latest SKILL.md from docmd-skills
DOCMD_FETCH_REMOTE_SKILL=1 docmd init # same, via env var
docker-publish: stopped firing on every push to main
The docker-publish.yml workflow had a push: branches: [main] trigger that caused it to run on every commit to main, not just on release events. Removed — it now only fires on v* tag pushes, published releases, and manual dispatch.
Dockerfile: pnpm install made reliable in CI
The production stage was using corepack prepare pnpm@10.33.2 --activate (with explicit version), but the builder stage’s corepack prepare --activate (no version) failed in GitHub Actions CI because it required a package.json in the current working directory.
Both stages now use npm install -g pnpm@10.33.2 directly — simpler, no corepack resolution steps, and works identically in CI and local.
On a clean build, pnpm also refused to remove the existing node_modules directory without interactive confirmation in the non-TTY Docker environment.
The install command now passes --config.confirmModulesPurge=false, which is pnpm’s targeted flag for this case.
The broader ENV CI=true route was considered and rejected because it leaks into every subsequent RUN.
UI translation keys render as literal text in published package
builtWith, copyContext, copiedToClipboard and other template-translated strings rendered as literal key text in built output when using the published @docmd/ui package.
The monorepo worked fine because packages/ui/translations/ existed alongside dist/, but the published package’s files array didn’t include the translations/ directory, so pnpm pack stripped it from the tarball.
Added translations to the files array; loadTranslations now also emits a clear warning when the directory is missing so future regressions don’t surface as silent literal-key output.
@docmd/template-* registry key mismatch
Setting theme.template to summer or @docmd/template-summer printed “Plugin ‘template-summer’ not found in official registry — manual installation required” even though the package was installed.
Root cause: the auto-install registry lookup stripped @docmd/plugin- from the package name to derive the registry key, but didn’t strip @docmd/template-.
So @docmd/template-summer resolved to the key template-summer instead of summer.
Both prefixes are now stripped correctly.
Custom/third-party templates were never affected — they bypass the registry lookup entirely.
--offline mode: navigation links don’t navigate (#164)
Clicking a navigation sidebar child link in an --offline build only toggled the parent’s collapse state instead of opening the target page.
Root cause: the data-spa-enabled flag stayed true for offline builds, so the SPA click handler bound and consumed clicks with preventDefault().
But file:// has no fetch for the SPA path-fetch, so the navigation never happened.
Fixed in two places:
data-spa-enabledis nowfalsewhenisOfflineModeis true, so the SPA handler never binds and the browser does normal full-page navigation.- A safety-net check in the main SPA click handler bails out for
file://protocol, so the click isn’t consumed even if the flag is wrong.
docmd build --offline # SPA now disabled automatically
# Workaround on 0.8.7: open via local server, or set layout.spa: false
--offline mode: comprehensive file:// robustness pass
While fixing #164, audited every other place the client-side JS touches localStorage, fetch, XMLHttpRequest, or relies on SPA/network.
All localStorage writes (sidebar collapse, theme toggle, locale preference, cookie consent) are now wrapped in try/catch so file:// builds don’t throw in Chromium-based browsers where local storage is blocked.
The version switcher’s HEAD fetch check is skipped under file:// (browsers block it anyway), going straight to direct navigation.
XHR in the noStyle i18n loader is wrapped with onerror handling.
URL builder already appended /index.html for offline mode (unchanged).
Result: opening a built site via file:// now works end-to-end with no JS errors, no broken navigation, and no silent feature loss.
📚 Documentation updates
The following docs were updated to reflect the fixes above:
- Deployment → Docker — Image Details table updated to reflect the root + su-exec model, and the “Custom working directory and file ownership” section no longer recommends the
-u $(id -u):$(id -g)workaround (no longer needed). docker/DOCKER.md— Permission Issues section rewritten.- All six READMEs (en, de, es, fr, ja, zh) — removed the
-u $(id -u):$(id -g)note.
The Docker docs will be extended in a follow-up to explicitly cover init --force, the -y flag, and the non-TTY behaviour.
🔒 Security foundation (Phase 0 — internal)
This release lands the shared security primitives that the rest of the security work in 0.8.8 builds on. These changes are non-breaking and purely additive — no public API change yet.
New safePath home and UserPath brand
safePath(root, relativePath) — the path-traversal guard required by DEVELOPMENT-BENCHMARK.md S1 — has moved from @docmd/api to @docmd/utils so it’s available everywhere without a dependency on the API package. @docmd/api continues to re-export it for backward compatibility.
Alongside it: a new UserPath TypeScript branded type. Any string cast via asUserPath() is marked as user-controlled input; downstream code is expected to resolve it through safePath() before any fs.* call. The brand is a runtime no-op (zero cost) and a type-system signal.
Centralised escape helpers
Four helpers now ship from @docmd/utils:
escHtml— HTML text and inline interpolationattrEsc— semantic alias for HTML attribute valuesjsonInject—JSON.stringifywrapper for<script>injectionscriptLiteral—JSON.stringifywrapper for inline JS string contexts
Every escape helper is unit-tested with XSS canary payloads (19 assertions). One canonical path means future contributors don’t reinvent .replace(/&/g, '&') patterns.
Two new internal ESLint rules
docmd/no-unsafe-fs-read— flagsfs.readFile,fs.writeFile,fs.unlinkSync, etc. where the path argument wasn’t resolved throughsafePath(). Catches potential CWE-22 violations at lint time. Currentlywarnseverity; promoted toerrorafter Phase 1 fixes the surface.docmd/require-verify-client— flags everynew WebSocketServer({...})without averifyClientcallback. Set toerrorimmediately so the two existing dev-server WebSocket instances (the dev server and the workspace dev server) fail CI until they get origin checks. Phase 1 PR 1.D adds the verifyClient callbacks.
The initial lint pass surfaces 141 path-traversal warnings and 2 verify-client errors across the monorepo — those become the Phase 1 cleanup backlog.
New security.html config key (default flipped to escape)
The markdown parser now respects a security.html config key with three values:
| Value | Behaviour | Use case |
|---|---|---|
'escape' (default in 0.8.8) |
Raw HTML in markdown is HTML-escaped and shown as text | Default — safe for user-generated content |
'allow' |
Raw HTML passes through to the rendered output | Documentation sites that intentionally use raw HTML like <details> blocks |
'strip' |
Raw HTML is removed from the rendered output entirely | Locked-down content (corporate wikis, regulated industries) |
Behaviour change: the pre-0.8.8 default was effectively
'allow'. If your docs rely on raw HTML (e.g.<details>,<summary>, embedded<iframe>), addsecurity: { html: 'allow' }to your config to keep the old behaviour. Container blocks (::: callout,::: tabs,::: steps,::: cards, etc.) are unaffected — they’re parsed by custom rules, not raw HTML.
End-to-end coverage lives in scripts/brute-test-security.js (22 assertions across all three policies plus fallback behaviour).
Path-traversal hardening (private disclosure)
Three internal file-access paths were hardened against path traversal. No public API change. See the security advisory for affected versions.
The fixes land in packages/core/src/commands/mcp.ts, packages/api/src/hooks.ts, and packages/plugins/openapi/src/index.ts.
All three go through the centralised safePath helper from @docmd/utils so any future code path is gated by the same primitive.
End-to-end coverage in scripts/brute-test-security.js scenarios S7 through S10 (15 assertions including a deliberate canary file outside the project root and a malicious local-path plugin fixture).
Dev-server hardening (private disclosure)
The dev-server WebSocket handlers and the init command were hardened.
The dev server now binds to 127.0.0.1 by default (use DOCMD_HOST=0.0.0.0 to expose on LAN) and both WebSocketServer instances validate the Origin header against a loopback allowlist before accepting the handshake.
The init command no longer makes any network call on every run — the bundled SKILL.md content is written locally; users install the full agent skill set via npx docmd-skills [dir].
End-to-end coverage in scripts/brute-test-security.js scenarios S11 and S12 (16 assertions including a snapshot of intercepted fetch calls proving the zero-network property).
HTML escape hardening (private disclosure)
User-controllable values interpolated into meta tags and plugin head/body injection points are now escaped via the centralised attrEsc / sanitizeHeadInjection helpers from @docmd/utils.
The og/twitter meta tags, link href schemes, and plugin-generated head HTML all go through the same canonical escape path.
Container blocks (::: callout, ::: tabs, etc.) were already protected by Phase 0’s default html: 'escape' markdown-it policy and require no further change.
End-to-end coverage in scripts/brute-test-security.js scenarios S13 through S15 (13 assertions including an in-project plugin whose generateMetaTags returns <script> and javascript: payloads).
Plugin input validation (private disclosure)
The analytics plugin now format-validates measurementId (must match G-XXXXXXXX) and trackingId (must match UA-XXXXXXX-Y) before injecting the Google Analytics script.
Invalid IDs are skipped with a console warning instead of being interpolated raw.
The PWA plugin now format-validates themeColor against a CSS color regex (#hex, rgb(), hsl(), or a CSS named color) and falls back to the default #1e293b on invalid input.
Both validators use scriptLiteral() / attrEsc() from @docmd/utils for defence-in-depth.
End-to-end coverage in scripts/brute-test-security.js scenarios S16 and S17 (15 assertions: invalid GA4 / UA / themeColor payloads, valid case, default-fallback path).
MCP and bundled SKILL.md are now docmd-skills-aware
docmd-skills is published as a standalone npm package (https://www.npmjs.com/package/docmd-skills). The bundled SKILL.md written by docmd init and the MCP get_skill fallback both now point agents at the single-line install command:
npx docmd-skills ~/.claude/skills # or ~/.cursor/skills, ./.skills, etc.
The full agent skill set (docmd-skills, docmd-dev, docmd-writer) is installed by that one command. The bundled SKILL.md is sufficient for MCP get_skill consumption; npx docmd-skills is the path to the rich, multi-file skill set for interactive agent use.
🛡️ Security hardening (Phase 1 — 13 CVEs closed)
The Battle Test 0.8.6 audit found 13 CVE-class issues in core + plugins.
All are fixed in 0.8.8.
CI pipelines that gated on docmd <cmd> exit codes were silently passing broken builds before these fixes.
Highlights
Cross-Site WebSocket Hijacking (CSWSH) — N-S1: dev-server WebSocket had no
verifyClientcallback, allowing any origin to open a connection and triggerthreads:add-threadfile writes.
NewverifyClientfactory (packages/core/src/utils/ws-origin-guard.ts) allows loopback by default and any explicit extra host.
Both WebSocket call sites indev.tsandworkspace.tsnow reject non-allowlisted origins with exit code 1.MCP read_doc path traversal — T-S1 / S-3:
path.resolve(cwd, route)atpackages/core/src/commands/mcp.tsaccepted absolute paths like/etc/passwdand../escapes.
Replaced withsafePath(cwd, asUserPath(route))which throws on traversal.
ThesafePathprimitive lives in@docmd/utilsand is the single source of truth for all file-resolution calls.Plugin RCE via local-subfolder — T-S8:
require.resolve(name, { paths: resolvePaths })atpackages/api/src/hooks.tssearched parent directories, loading arbitrary npm packages from sibling folders.
Local-path plugins are now resolved viasafePath(projectRoot, asUserPath(name))which restricts to the project root.Plugin head XSS — T-S7: a plugin returning
<script>alert(1)</script>fromgenerateMetaTagsinjected live script into every page.
NewsanitizeHeadInjection()in@docmd/utilsstrips<script>/<style>and neutralisesjavascript:/vbscript:URIs.
Wired intogenerator.tsforinjectHead/injectBody.OpenAPI / Analytics / PWA / Threads XSS — T-S3 / S-4 / S-5 / S-7: all four plugins now format-validate their untrusted config inputs (GA4
measurementId, UAtrackingId, PWAthemeColor) and escape viaattrEsc()/scriptLiteral()from@docmd/utilsbefore interpolating into the HTML.Init makes a network call to GitHub — T-S5: removed.
docmd-skillsis now a standalone npm package — the bundledSKILL.mdis the default; opt into the network call only via the legacy--with-skillflag (kept for back-compat).Dev server binds to
0.0.0.0by default — T-S6: default flipped to127.0.0.1.
TheDOCMD_HOSTenv var opts into a LAN bind.
The TUI warns when binding to a non-loopback address.
New helpers in @docmd/utils
| Function | Purpose |
|---|---|
escHtml(s) |
HTML-escape text and <script> blocks |
attrEsc(s) |
HTML-escape attribute values |
jsonInject(s) |
Safely inject into JSON context |
scriptLiteral(s) |
Safely inject into JS string literal |
safePath(root, userPath) |
Path-traversal-safe file resolution |
asUserPath(s) |
Branded UserPath type |
normalisePath(s) |
POSIX path normalisation |
sanitizeHeadInjection(html) |
Strip <script>, <style>, neutralise javascript: / vbscript: |
Test coverage
scripts/brute-test-security.js — 88 assertions across 13 scenarios (S-1, S-3, S-4, S-5, S-6, S-7, S-9, S-10, S-12, S-13, S-15, S-16, S-17) covering every documented CVE path.
All 13 issues are regression-tested at the CLI surface — a CI run catches any regression automatically.
📦 Container parser (Phase 2 — 5 bugs fixed)
The 0.8.6 audit found five silent container-parser bugs (F1–F5) that produced wrong output with no error and no exit-code change.
The parser is now deterministic, brace-aware, and indentation-aware.
What was fixed
- F1 — nested grids:
::: grids+ N×::: grid+ one:::per card used to dump the whole block as raw<p>::: grids<br>::: grid<br>...<br>:::</p>text.
The normaliser adds explicit:::closes so thegridsrule matches. - F2 — self-closing tag corruption:
::: tag+ orphan:::corrupted the depth counter for every container that followed.
Self-closing names (button,tag,embed) are honoured; the orphan:::is removed with a[normaliser] WARNING. - F3 — mismatched close types:
::: callout ... ::: card ... :::silently re-rooted the parser.
The callout is auto-closed at EOF with a[normaliser] ERROR. - F4 — triple close erases content: bare
:::lines leaked into the page as<p>:::</p>paragraphs.
Orphans are removed with warnings. - F5 — 5-level nested callouts collapse: nested callouts rendered only the outer level.
The normaliser inserts implicit closes for the inner levels so eachcalloutrule matches recursively.
Implementation
New packages/parser/src/utils/container-normaliser.ts — pure TypeScript port of the legacy robust-parser-shim/ (146 lines).
Linear scan, indentation-aware stack, no module-level mutable state.
Wired into packages/parser/src/markdown-processor.ts in BOTH processContent and processContentAsync BEFORE user plugins’ onBeforeParse and BEFORE markdown-it’s render.
Re-runs inside createDepthTrackingContainer BEFORE smartDedent so the inner recursive render sees balanced input.
Determinism contract
The normaliser is a pure function of its input — no Date.now, no Math.random, no module-level mutable state.
Three layers guard the contract:
- Declarative —
DETERMINISM AUDITblock in the file header documents the four non-deterministic primitives the module does NOT use. - Empirical —
packages/parser/test/container-normaliser.test.jshas 60 assertions including 100-way concurrent parse AND a realnode:worker_threadscross-worker parse. - Regression-proof —
verifyDeterminismAtBoot()inpackages/core/src/engine/worker-parser.tsparses a known input at worker init and asserts the output matches a frozen snapshot.
Any future non-determinism crashes the worker at boot.
🔧 CLI contracts (Phase 3 — 7 bugs fixed)
Five documented CLI failure paths silently exited 0; two validate paths returned wrong results.
All seven are fixed.
What was fixed
| Command | Failure case | Pre-fix exit | Post-fix exit |
|---|---|---|---|
docmd build |
Unknown plugin in config | 0 | 1 |
docmd migrate |
No --docusaurus/etc. flag |
0 | 1 |
docmd migrate --help |
(it’s a help print) | 0 | 0 (unchanged) |
docmd remove <nonexistent> |
Plugin not in registry | 0 | 1 |
docmd validate --json |
Broken links | 0 | 1 |
docmd add <plugin> on TS/MJS/CJS |
Config file untouched | 0 | 1 + fixed |
docmd remove <plugin> |
Plugin entry left in config | 0 | 1 + fixed |
docmd build (workspace) |
Raw stack trace, exit 0 | 0 | 1 + clean TUI |
docmd init example |
Taught F2 trap pattern | OK | cleaned up |
docmd validate (trailing slash) |
False-positive on /page/ |
1 | 0 |
Implementation
- New
packages/core/src/utils/exit.tsexportsexitCodeFor(err),exitWithError(err, opts?),failWith(message, opts?)— the single source of truth for the exit-code contract. - New
getPluginLoadErrors()in@docmd/apitracks plugin LOAD failures separately from RUNTIME hook errors.
build.tschecks it afterloadPlugins()and throws, which the existing catch block turns into exit 1. - New
packages/plugins/installer/src/config-editor.ts— a brace-balanced scanner that handles all five supported config formats (docmd.config.{json,js,mjs,cjs,ts}) uniformly.
The legacy regex-based injector only matchedmodule.exports = {...}and silently no-op’d forexport default defineConfig({...}).
resolveConfigPath()was checking only.json/.jsand would have silently scaffolded a new file next to a project’s.ts.
Test coverage
scripts/brute-test.js — 22 new assertions across three Phase 3 sections (test 26: exit codes, test 27: plugin add/remove, test 29: validate + workspace + init).
All under tests/cli-contracts/.
🤖 OKF plugin (0.8.8 — new core plugin)
A new core plugin generates an Open Knowledge Format (Google’s OKF spec) bundle for AI-agent consumption.
The bundle sits at site/okf/ and contains a typed manifest (okf.yaml), an interactive graph viewer, and one markdown file per page.
What you get out of the box
site/okf/
├── okf.yaml ← typed manifest (bundle summary)
├── index.md ← Karpathy-style catalog grouped by type
├── graph.html ← interactive force-directed viewer
├── graph.json ← graph data (nodes + edges)
├── graph.js ← viewer runtime (vanilla, no CDN deps)
├── graph.css ← viewer styles (theme-aware)
├── concepts/<slug>.md ← one markdown file per page
└── _meta/
├── bundle.json ← JSON mirror of okf.yaml
└── lint-report.txt ← warnings produced during generation
Default behaviour (0.8.8)
- Default-enabled — OKF is a core plugin like
llmsandseo.
The build auto-loads it and generates the bundle even when the user hasn’t addedplugins.okfto their config.
Three opt-out paths are supported (false,enabled: false, or capability filter). - Single-locale by default — the bundle contains pages in the default locale only.
The default locale’s files sit at the bundle root (noen/subdirectory). - Type inference — pages under
/api/,/guides/, etc. are auto-classified; everything else falls back toconcept.
Multi-locale opt-in
{
"plugins": {
"okf": { "localeStrategy": "folders" },
"llms": { "i18n": true }
}
}
With both flags set, the default-locale files stay at the bundle root (so existing consumers don’t break) and non-default locales get a <locale>/ subdirectory (OKF) or .<locale> suffix (LLMS):
site/okf/okf.yaml ← default locale
site/okf/concepts/...md ← default locale concepts
site/okf/ja/okf.yaml ← Japanese
site/okf/ja/concepts/...md
site/llms.txt ← default locale (en)
site/llms.ja.txt ← Japanese
Test coverage
packages/plugins/okf/tests/index.test.ts (6 tests) + packages/plugins/okf/tests/brute.test.ts (15 tests) including 12 real-fs scenarios.
21/21 pass.
New test exercises the default-enabled contract (a project with no plugins.okf entry still gets a bundle generated).
🧪 Test infrastructure (categorised)
The brute tests used to be 1000+ lines in two scripts/ files with duplicate test numbers (Phase 3 PR 3.A reused 26, 27, 29).
They are now categorised under tests/:
tests/
├── shared.js ← fixture helpers
├── runner.js ← orchestrator with TUI sections
├── cli-contracts/
│ ├── exit-codes.test.js ← F6, M-12
│ ├── plugin-add-remove.test.js ← F7, M-3
│ └── validate-workspace.test.js ← M-1, F8, F9
└── (per-package tests stay in place under packages/*/test/)
The runner is invoked by pnpm prep (or directly via node tests/runner.js) and runs each suite in its own TUI section.
Filter by --only=exit-codes,container-normaliser to run just the named suites.
Result
342 assertions across 9 categorised files, all green (with per-package unit tests adding 89 more across 4 packages, totalling 431 assertions / 13 files):
| Suite | Result | Coverage |
|---|---|---|
cli-contracts/exit-codes |
7 / 7 | F6, M-12 |
cli-contracts/plugin-add-remove |
9 / 9 | F7, M-3 (TS, MJS, JSON, CJS, JS) |
cli-contracts/validate-workspace |
6 / 6 | M-1, F8, F9 |
packages/parser/test/container-normaliser |
60 / 60 | F1–F5 + determinism |
packages/utils/test/{path,html-escape} |
31 / 31 | safePath, escHtml, etc. |
scripts/brute-test-security |
88 / 88 | Phase 1 CVE suite (13 scenarios) |
scripts/brute-test |
114 / 114 | 29 feature-integration scenarios |
packages/plugins/okf |
23 / 23 | Unit + 12 real-fs scenarios + graph opt-in/deprecation |
packages/plugins/llms |
6 / 6 | Default-locale + i18n opt-in (NEW) |
Updates — pnpm prep pipeline hardened
The release pipeline gained three operational improvements:
- Per-package unit tests now participate in the gate.
pnpm -r run test --if-presentruns aftertests/runner.js, so a regression in@docmd/parser,@docmd/utils,@docmd/plugin-okf, or@docmd/plugin-mermaidfails the release pipeline the same way a categorised-runner regression does. No package-level test suite can drift silently anymore. --verbose/--fullflag. Default output stays collapsed (each test step is one line) so a clean run is readable at a glance. Pass--verboseto stream the raw test output inline — useful when iterating on a test that just started failing and you want the assertion text in context.- Trailing
Summaryblock. On a clean run, every section contributes a stat (lint counts, package totals, test counts, Docker version) that renders as one green block at the end. On failure, the same slot becomes a redIssuesblock listing every failure with file:line detail and capped overflow. Single place to look for the verdict.
🔌 Plugin author note
Both @docmd/plugin-llms and @docmd/plugin-okf are default-enabled core plugins in 0.8.8.
The build auto-loads them.
To opt out, use the enabled: false shape that already works for every other core plugin:
{
"plugins": {
"okf": { "enabled": false },
"llms": { "enabled": false }
}
}
Looking ahead — Node 20+ in v0.9.0
The next minor release raises the floor to Node.js 20+ (from 18+), bringing the engine requirement, shipped Docker runtime, and CI environment all onto Node 22.
Node 18 reached end-of-life in April 2025; this keeps docmd on a supported runtime with room to spare.
This release (0.8.8) is unaffected — the >=18 floor stays until 0.9.0 ships.
| Now (0.8.x) | Next (0.9.0) | |
|---|---|---|
| Minimum Node | 18+ | 20+ |
| Docker base | 20-alpine |
20-alpine |
| Type Node | ^24 | ^24 |
| CI target | 24 | 24 |
Affected versions
All fixes in this release target 0.8.7. There are no breaking changes — every fix is additive or pure bug correction.