Skip to content

feat(cli): add --batch directory conversion (#77) - #89

Open
HaoChiBao wants to merge 1 commit into
firecrawl:mainfrom
HaoChiBao:feat/batch-cli-77
Open

feat(cli): add --batch directory conversion (#77)#89
HaoChiBao wants to merge 1 commit into
firecrawl:mainfrom
HaoChiBao:feat/batch-cli-77

Conversation

@HaoChiBao

@HaoChiBao HaoChiBao commented Aug 13, 2026

Copy link
Copy Markdown

Summary

  • Add anydoc --batch <dir> -o <dir> to the Node CLI for recursive directory conversion
  • Preserve relative paths; write converted files as original.ext.md; copy .txt/.json/.md/.py as-is; skip unsupported files silently
  • Continue on per-file conversion failures and exit 1 if any failed; document overwrite and exit-code behavior in --help

Closes #77

Notes

Test plan

  • cd node && node --test (16 tests, including 3 batch cases)
  • Manual: anydoc --batch ./sample -o ./out and confirm tree / *.ext.md / passthrough / skips

Summary by cubic

Adds --batch directory conversion to the Node CLI to recursively convert trees and copy selected plaintext files as-is. Previously the CLI converted one file per run; now it supports --batch <dir> -o <dir> with preserved relative paths and safer error handling, enabling bulk migrations (addresses #77).

  • Converts supported files to Markdown named original.ext.md; copies .txt/.json/.md/.py unchanged; skips unsupported files silently; overwrites existing outputs; --batch does not read stdin.
  • Requires -o <dir> with --batch; rejects --format in batch mode; accepts --batch <dir> or --batch followed by a positional dir.
  • Continues past per-file failures; logs errors to stderr; exits 1 if any file failed, 2 on usage errors, 0 on success. Single-file behavior is unchanged.
  • Implementation: new runBatch, walkFiles, and passthrough handling; deferred binding load remains. Tests add batch success/failure/usage cases; README and Node README document the new flag.

Rollout/Migration

  • Update scripts to use anydoc --batch <inputDir> -o <outputDir>; do not pass --format in batch mode.
  • Expect exit code 1 when any file fails and plan CI accordingly. No changes needed for non-batch usage.

Written for commit fa2cfee. Summary will update on new commits.

Review in cubic

Supports recursive convert-and-passthrough of folder trees for issue firecrawl#77 without racing the Rust CLI work in firecrawl#29.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="node/test.mjs">

<violation number="1" location="node/test.mjs:152">
P2: The batch test covers only `.txt` passthrough files, so regressions in the documented `.json`, `.md`, or `.py` passthrough behavior will go undetected. Add fixtures and byte-for-byte output assertions for all four extensions.</violation>
</file>

<file name="node/cli.js">

<violation number="1" location="node/cli.js:191">
P2: When `-o` points into (or equals) the input directory tree — e.g. `anydoc --batch ./docs -o ./docs/out` — the output directory is created before walking, so `walkFiles` descends into it and re-processes its contents. Freshly written `*.md` files and copied passthrough files get detected, converted/copied again, and passthrough copies can land back inside the tree; on repeated runs the prior output is re-consumed as input. There is no guard rejecting or warning when the output root is inside the input root. Refuse (or at least warn) when `relative(inputRoot, outputRoot)` is not strictly outside the input tree.</violation>

<violation number="2" location="node/cli.js:197">
P2: When a nested directory cannot be read, `walkFiles` rejects outside the per-file error handling and aborts the batch. Catch traversal errors per directory so sibling files still run and the command reports exit 1 with a concise diagnostic.</violation>

<violation number="3" location="node/cli.js:200">
P3: This path-escape guard is unreachable. `absPath` is always produced by `join(inputRoot, entry.name)` inside `walkFiles`, and `inputRoot` is `resolve()`d, so `relative(inputRoot, absPath)` can never be `..` or start with `../` — its first path segment is always a child of the input root. The `continue` is dead code that can mislead future readers into thinking escaping paths are possible. Consider dropping it (and the now-misleading comment) or replacing it with a comment explaining why it cannot occur.</violation>

<violation number="4" location="node/cli.js:214">
P2: Batch skips valid documents when their filenames lack a recognized extension. `toMarkdown` supports content-based detection before extension fallback; use content detection before treating a file as unsupported, while still silently skipping genuinely unsupported content.</violation>

<violation number="5" location="node/cli.js:219">
P2: When a converted file and a passthrough `.md` file share a target name, batch silently overwrites one source's output. Detect duplicate output paths and report the collision instead of allowing data loss.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread node/test.mjs
await mkdir(join(input, 'nested'), { recursive: true })
await copyFile(OUTLINE, join(input, 'nested', 'handbook.docx'))
await copyFile(CSV, join(input, 'sheet.csv'))
await writeFile(join(input, 'notes.txt'), 'plain text\n')

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The batch test covers only .txt passthrough files, so regressions in the documented .json, .md, or .py passthrough behavior will go undetected. Add fixtures and byte-for-byte output assertions for all four extensions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node/test.mjs, line 152:

<comment>The batch test covers only `.txt` passthrough files, so regressions in the documented `.json`, `.md`, or `.py` passthrough behavior will go undetected. Add fixtures and byte-for-byte output assertions for all four extensions.</comment>

<file context>
@@ -136,6 +136,64 @@ test('cli help and version go to stdout and exit 0', async () => {
+    await mkdir(join(input, 'nested'), { recursive: true })
+    await copyFile(OUTLINE, join(input, 'nested', 'handbook.docx'))
+    await copyFile(CSV, join(input, 'sheet.csv'))
+    await writeFile(join(input, 'notes.txt'), 'plain text\n')
+    await writeFile(join(input, 'nested', 'skip.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47]))
+
</file context>
Fix with cubic

Comment thread node/cli.js
continue
}

const outPath = join(outputRoot, `${rel}.md`)

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a converted file and a passthrough .md file share a target name, batch silently overwrites one source's output. Detect duplicate output paths and report the collision instead of allowing data loss.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node/cli.js, line 219:

<comment>When a converted file and a passthrough `.md` file share a target name, batch silently overwrites one source's output. Detect duplicate output paths and report the collision instead of allowing data loss.</comment>

<file context>
@@ -113,16 +146,93 @@ async function readStdin() {
+      continue
+    }
+
+    const outPath = join(outputRoot, `${rel}.md`)
+    try {
+      const markdown = await toMarkdown(absPath)
</file context>
Fix with cubic

Comment thread node/cli.js
}

let failed = 0
for await (const absPath of walkFiles(inputRoot)) {

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a nested directory cannot be read, walkFiles rejects outside the per-file error handling and aborts the batch. Catch traversal errors per directory so sibling files still run and the command reports exit 1 with a concise diagnostic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node/cli.js, line 197:

<comment>When a nested directory cannot be read, `walkFiles` rejects outside the per-file error handling and aborts the batch. Catch traversal errors per directory so sibling files still run and the command reports exit 1 with a concise diagnostic.</comment>

<file context>
@@ -113,16 +146,93 @@ async function readStdin() {
+  }
+
+  let failed = 0
+  for await (const absPath of walkFiles(inputRoot)) {
+    const rel = relative(inputRoot, absPath)
+    // Guard against path escape on odd relative() results.
</file context>
Fix with cubic

Comment thread node/cli.js
continue
}

if (formatFromPath(absPath) === null) {

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Batch skips valid documents when their filenames lack a recognized extension. toMarkdown supports content-based detection before extension fallback; use content detection before treating a file as unsupported, while still silently skipping genuinely unsupported content.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node/cli.js, line 214:

<comment>Batch skips valid documents when their filenames lack a recognized extension. `toMarkdown` supports content-based detection before extension fallback; use content detection before treating a file as unsupported, while still silently skipping genuinely unsupported content.</comment>

<file context>
@@ -113,16 +146,93 @@ async function readStdin() {
+      continue
+    }
+
+    if (formatFromPath(absPath) === null) {
+      // Unsupported (images, videos, archives, unknown extensions): skip.
+      continue
</file context>
Fix with cubic

Comment thread node/cli.js
const outputRoot = resolve(args.output)

try {
await mkdir(outputRoot, { recursive: true })

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When -o points into (or equals) the input directory tree — e.g. anydoc --batch ./docs -o ./docs/out — the output directory is created before walking, so walkFiles descends into it and re-processes its contents. Freshly written *.md files and copied passthrough files get detected, converted/copied again, and passthrough copies can land back inside the tree; on repeated runs the prior output is re-consumed as input. There is no guard rejecting or warning when the output root is inside the input root. Refuse (or at least warn) when relative(inputRoot, outputRoot) is not strictly outside the input tree.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node/cli.js, line 191:

<comment>When `-o` points into (or equals) the input directory tree — e.g. `anydoc --batch ./docs -o ./docs/out` — the output directory is created before walking, so `walkFiles` descends into it and re-processes its contents. Freshly written `*.md` files and copied passthrough files get detected, converted/copied again, and passthrough copies can land back inside the tree; on repeated runs the prior output is re-consumed as input. There is no guard rejecting or warning when the output root is inside the input root. Refuse (or at least warn) when `relative(inputRoot, outputRoot)` is not strictly outside the input tree.</comment>

<file context>
@@ -113,16 +146,93 @@ async function readStdin() {
+  const outputRoot = resolve(args.output)
+
+  try {
+    await mkdir(outputRoot, { recursive: true })
+  } catch (error) {
+    fail(CONVERSION_ERROR, error.message)
</file context>
Fix with cubic

Comment thread node/cli.js
for await (const absPath of walkFiles(inputRoot)) {
const rel = relative(inputRoot, absPath)
// Guard against path escape on odd relative() results.
if (rel.startsWith(`..${sep}`) || rel === '..') continue

@cubic-dev-ai cubic-dev-ai Bot Aug 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This path-escape guard is unreachable. absPath is always produced by join(inputRoot, entry.name) inside walkFiles, and inputRoot is resolve()d, so relative(inputRoot, absPath) can never be .. or start with ../ — its first path segment is always a child of the input root. The continue is dead code that can mislead future readers into thinking escaping paths are possible. Consider dropping it (and the now-misleading comment) or replacing it with a comment explaining why it cannot occur.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node/cli.js, line 200:

<comment>This path-escape guard is unreachable. `absPath` is always produced by `join(inputRoot, entry.name)` inside `walkFiles`, and `inputRoot` is `resolve()`d, so `relative(inputRoot, absPath)` can never be `..` or start with `../` — its first path segment is always a child of the input root. The `continue` is dead code that can mislead future readers into thinking escaping paths are possible. Consider dropping it (and the now-misleading comment) or replacing it with a comment explaining why it cannot occur.</comment>

<file context>
@@ -113,16 +146,93 @@ async function readStdin() {
+  for await (const absPath of walkFiles(inputRoot)) {
+    const rel = relative(inputRoot, absPath)
+    // Guard against path escape on odd relative() results.
+    if (rel.startsWith(`..${sep}`) || rel === '..') continue
 
+    if (isPassthrough(absPath)) {
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Batch mode for bulk directory level processing

1 participant