From 87319ae7dd760514e98504f11ba915a43f1e3939 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Fri, 28 Aug 2026 13:45:19 +1000 Subject: [PATCH 1/3] test: cover continued markdown commands --- examples/fixtures/docs-only/docs/runbook.md | 6 ++++++ tests/scan.test.mjs | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/examples/fixtures/docs-only/docs/runbook.md b/examples/fixtures/docs-only/docs/runbook.md index 575e194..ac8c759 100644 --- a/examples/fixtures/docs-only/docs/runbook.md +++ b/examples/fixtures/docs-only/docs/runbook.md @@ -19,3 +19,9 @@ docker compose up sudo chmod 600 .env npm test ``` + +```bash +rm \ + -rf ./generated +CI=1 npm test +``` diff --git a/tests/scan.test.mjs b/tests/scan.test.mjs index 1f20383..01b5e7d 100644 --- a/tests/scan.test.mjs +++ b/tests/scan.test.mjs @@ -43,6 +43,17 @@ test("scans risk-relevant commands in shell documentation", async () => { assert.equal(commands.get("npm test")?.risk, "safe"); }); +test("joins shell continuations and recognizes environment assignments", async () => { + const report = await scanProject({ root: fixture("docs-only") }); + const commands = new Map(report.commands.map((command) => [command.command, command])); + + assert.equal(commands.get("rm -rf ./generated")?.risk, "dangerous"); + assert.equal(commands.get("rm -rf ./generated")?.location.line, 24); + assert.equal(commands.get("CI=1 npm test")?.risk, "safe"); + assert.equal(commands.get("CI=1 npm test")?.location.line, 26); + assert.equal(commands.has("rm \\"), false); +}); + test("scans only executable Taskfile commands", async () => { const report = await scanProject({ root: fixture("taskfile-metadata") }); const taskCommands = report.commands.filter((command) => command.kind === "taskfile"); From 04e33117e18f57115408e8fb4fc23746bdaddef7 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Fri, 28 Aug 2026 13:45:49 +1000 Subject: [PATCH 2/3] fix: parse continued markdown commands --- src/discover/markdown.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/discover/markdown.ts b/src/discover/markdown.ts index 7a79559..5f2202a 100644 --- a/src/discover/markdown.ts +++ b/src/discover/markdown.ts @@ -2,7 +2,7 @@ import path from "node:path"; import { findFilesByExtension, readText, toPosixRelative } from "../files.js"; import type { CommandSource } from "../types.js"; -const COMMAND_PREFIX = /^(?:\$\s*)?(?:npm|pnpm|yarn|node|npx|bash|sh|make|just|task|deno|bun|tsx|curl|wget|docker(?:-compose)?|git|gh|rm|sudo|chmod|chown|vercel|flyctl|netlify|changeset)\b/; +const COMMAND_PREFIX = /^(?:(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+))\s+)*(?:npm|pnpm|yarn|node|npx|bash|sh|make|just|task|deno|bun|tsx|curl|wget|docker(?:-compose)?|git|gh|rm|sudo|chmod|chown|vercel|flyctl|netlify|changeset)\b/; export async function discoverMarkdownCommands(root: string): Promise { const files = await findFilesByExtension(root, ".md"); @@ -49,20 +49,25 @@ function extractCodeBlockCommands(relativeFile: string, text: string): CommandSo continue; } - const command = line.trim().replace(/^\$\s*/, ""); + const sourceLine = index + 1; + let command = line.trim().replace(/^\$\s*/, ""); + while (/\\$/.test(command) && index + 1 < lines.length) { + command = `${command.slice(0, -1).trimEnd()} ${lines[index + 1].trim()}`; + index += 1; + } if (!COMMAND_PREFIX.test(command)) { continue; } const basename = path.basename(relativeFile, ".md").toLowerCase(); commands.push({ - id: `${relativeFile}#code-${index + 1}`, - name: `${basename}:line-${index + 1}`, + id: `${relativeFile}#code-${sourceLine}`, + name: `${basename}:line-${sourceLine}`, command, kind: "markdown", location: { file: relativeFile, - line: index + 1 + line: sourceLine } }); } From 79f2a7ff9c402cad93f723d2508a8e6ea814c03a Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Fri, 28 Aug 2026 13:46:06 +1000 Subject: [PATCH 3/3] docs: define markdown shell forms --- CHANGELOG.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c7e86..60764fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ format and uses semantic versioning when versioned releases are published. ## [Unreleased] +### Fixed + +- Join shell continuations and recognize leading environment assignments when + discovering commands in Markdown fences. + ### Added - Added verified npm trusted publication with provenance and reuse of the packed artifact in GitHub releases. diff --git a/README.md b/README.md index a70852a..64edeb9 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ For a fixture-backed walkthrough, see [`docs/tutorials/audit-agent-cli-scripts.m - `package.json` scripts across the repository. - `pnpm-workspace.yaml` workspace hints. - Makefile targets. -- Markdown shell blocks fenced with backticks or tildes and labelled `bash`, `sh`, `shell`, `console`, or `zsh` (unlabelled fences are also scanned). Each independently executable line must begin with a supported command or an optional `$` prompt. Supported commands include package and task runners, Node and shell entry points, plus risk-relevant network, container, version-control, destructive, permission, deploy, and publish tools such as `curl`, `docker`, `git`, `rm`, `sudo`, and `vercel`. +- Markdown shell blocks fenced with backticks or tildes and labelled `bash`, `sh`, `shell`, `console`, or `zsh` (unlabelled fences are also scanned). Shell lines ending in `\` are joined before classification, and commands may begin with standard environment assignments such as `CI=1 npm test`. Each independently executable command must otherwise begin with a supported command or an optional `$` prompt. Supported commands include package and task runners, Node and shell entry points, plus risk-relevant network, container, version-control, destructive, permission, deploy, and publish tools such as `curl`, `docker`, `git`, `rm`, `sudo`, and `vercel`. - Justfile recipes and Taskfile `cmds` entries written as scalar commands (`- npm test`) or inline mappings (`- cmd: npm test`). Make discovery recognizes ordinary named targets, including rules that list