diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c3e70..09dcf50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ Versions before 0.8.0 are in the [git tags](https://github.com/leemour/cli-core/ Every entry says what changed as a caller sees it, why, and what to watch for — the rules are [`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog). +## Unreleased + +### Fixed + +- **`cli-dev docs-check` and `markdownFiles` skip what is not a file or a folder.** A device or a + pipe named `*.md` used to be read, and the check crashed. Claude Code's Bash sandbox lays devices + over names in the working folder (`.claude/loop.md`, `.claude/skills`), so the check failed in + every agent session with the sandbox on. A symbolic link to a file is still listed; a link to a + folder is still not followed. + ## 0.17.0 — 03.10.2026 ### Changed — may break callers diff --git a/src/release/markdown.ts b/src/release/markdown.ts index c1dcf3a..53e9ce6 100644 --- a/src/release/markdown.ts +++ b/src/release/markdown.ts @@ -106,8 +106,11 @@ export const markdownFiles = (directory: string, skip: ReadonlySet = new ? readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { if (skip.has(entry.name)) return [] const path = join(directory, entry.name) - if (entry.isDirectory()) return markdownFiles(path, skip) - return entry.name.endsWith(".md") ? [path] : [] + // stat, not the entry's own type: Claude Code's sandbox mounts character devices over names + // like .claude/loop.md, and the directory listing still reports what lies underneath. + const stats = statSync(path, { throwIfNoEntry: false }) + if (entry.isDirectory() && stats?.isDirectory()) return markdownFiles(path, skip) + return stats?.isFile() && entry.name.endsWith(".md") ? [path] : [] }) : [] diff --git a/src/release/release.test.ts b/src/release/release.test.ts index c2d6307..2c5c94d 100644 --- a/src/release/release.test.ts +++ b/src/release/release.test.ts @@ -1,4 +1,5 @@ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" +import { execFileSync } from "node:child_process" +import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { dirname, join } from "node:path" import { afterEach, beforeEach, describe, expect, it } from "vitest" @@ -168,6 +169,18 @@ describe("slug", () => { }) }) +describe("markdownFiles", () => { + it.skipIf(process.platform === "win32")("lists files and links, not a special file with a .md name", () => { + const root = mkdtempSync(join(tmpdir(), "markdown-files-")) + writeFileSync(join(root, "README.md"), "# x\n") + symlinkSync(join(root, "README.md"), join(root, "linked.md")) + execFileSync("mkfifo", [join(root, "loop.md")]) + + expect(markdownFiles(root).sort()).toEqual([join(root, "README.md"), join(root, "linked.md")]) + rmSync(root, { recursive: true, force: true }) + }) +}) + describe("docsProblems", () => { let root: string const write = (path: string, text: string) => {