jackwener/OpenCLI · error · CommandExecutionError

Failed to write Pixiv novel ${path.basename(plan.destPath)}:

Error message

Failed to write Pixiv novel ${path.basename(plan.destPath)}: ${error?.message || error}

What it means

commitNovelFile performs the actual mkdir/write (mkdirSync + openSync with 'wx' flag) and rolls back on failure. Any error that is not already a CommandExecutionError (e.g. ENOSPC disk full, EACCES, EMFILE, EIO) is wrapped in a CommandExecutionError prefixed with 'Failed to write Pixiv novel <basename>' plus the underlying OS message.

Source

Thrown at clis/pixiv/novel-download-utils.js:198

export function commitNovelFile(plan) {
  let descriptor;
  try {
    fs.mkdirSync(plan.outputDir, { recursive: true });
    descriptor = fs.openSync(plan.destPath, 'wx');
    fs.writeFileSync(descriptor, plan.content, 'utf8');
    fs.closeSync(descriptor);
    descriptor = undefined;
    return plan.destPath;
  } catch (error) {
    if (descriptor !== undefined) {
      try { fs.closeSync(descriptor); } catch {}
      try { fs.rmSync(plan.destPath, { force: true }); } catch {}
    }
    for (const directory of plan.createdDirs) {
      try { fs.rmdirSync(directory); } catch {}
    }
    if (error instanceof CommandExecutionError) throw error;
    throw new CommandExecutionError(`Failed to write Pixiv novel ${path.basename(plan.destPath)}: ${error?.message || error}`);
  }
}

export function cleanupNovelFile(plan) {
  try { fs.rmSync(plan.destPath, { force: true }); } catch {}
  for (const directory of plan.createdDirs) {
    try { fs.rmdirSync(directory); } catch {}
  }
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read error.message in the thrown CommandExecutionError to identify the OS cause (ENOSPC -> free disk space; EACCES -> fix permissions; EMFILE -> raise ulimit -n or reduce concurrency).
  2. Free disk space / raise quota, then re-run after running cleanupNovelFile to remove partial artifacts.
  3. Ensure only one process downloads into the same output directory to avoid 'wx' EEXIST races.
  4. Reduce parallelism for large batches; retry the download once the transient cause (mount, space, fds) is fixed.

Example fix

// before
await Promise.all(ids.map(id => writeNovelFile(bodies[id], out))); // EMFILE
// after
import pLimit from 'p-limit';
const limit = pLimit(4);
await Promise.all(ids.map(id => limit(() => writeNovelFile(bodies[id], out))));
Defensive patterns

Strategy: try-catch

Validate before calling

import fs from 'node:fs';
function assertWritable(dir) {
  fs.mkdirSync(dir, { recursive: true });
  fs.accessSync(dir, fs.constants.W_OK);
  const stat = fs.statfsSync(dir);
  if (stat.bavail * stat.bsize < 10 * 1024 * 1024) throw new Error('Less than 10MB free');
}

Try / catch

try {
  await downloadNovel(id, { output });
} catch (e) {
  if (e.message.startsWith('Failed to write Pixiv novel')) {
    console.error(`Write failed: ${e.message}`);
    if (e.message.includes('ENOSPC')) freeDiskSpace();
    if (e.message.includes('EACCES')) fixPermissions();
  } else throw e;
}

Prevention

When it happens

Trigger: fs.mkdirSync fails (permission denied on outputDir), openSync 'wx' fails (file appeared between prepare and commit — EEXIST; disk full — ENOSPC; too many open files — EMFILE), or writeFileSync fails with EIO mid-write.

Common situations: Disk quota exceeded or full disk when downloading large batches; another process creating the same destination between the exists-check and the write (TOCTOU with 'wx'); running out of file descriptors in large parallel batches; output folder made read-only mid-run.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/2a804c1346b5e803. Report an issue: GitHub.