jackwener/OpenCLI · error · ArgumentError

output must be a non-empty directory path

Error message

output must be a non-empty directory path

What it means

After type-checking, normalizePixivOutputRoot resolves the output value against the fallback and rejects values that are empty (after nullish coalescing with the fallback) or that contain a NUL byte ('\0'), which Node filesystem APIs reject. This guards against producing an unusable or malicious destination path.

Source

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

export function normalizeNovelFileFormat(value) {
  if (value !== undefined && typeof value !== 'string') {
    throw new ArgumentError('Novel download format must be txt or md');
  }
  const format = (value ?? 'txt').toLowerCase();
  if (format !== 'txt' && format !== 'md') {
    throw new ArgumentError(`Unsupported novel download format: ${format}. Supported formats: txt, md.`);
  }
  return format;
}

export function normalizePixivOutputRoot(value, fallback) {
  if (value !== undefined && typeof value !== 'string') {
    throw new ArgumentError('output must be a directory path');
  }
  const raw = value ?? fallback;
  if (!raw || raw.includes('\0')) {
    throw new ArgumentError('output must be a non-empty directory path');
  }
  const resolved = path.resolve(raw);
  let ancestor = resolved;
  const missingParts = [];
  let ancestorStat;
  while (!ancestorStat) {
    try {
      ancestorStat = fs.lstatSync(ancestor);
    } catch (error) {
      if (error?.code !== 'ENOENT') {
        throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);
      }
      const parent = path.dirname(ancestor);
      if (parent === ancestor) {
        throw new ArgumentError(`output path is not a safe directory: ${resolved}`);
      }
      missingParts.unshift(path.basename(ancestor));
      ancestor = parent;

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Provide a non-empty directory path string, e.g. './pixiv-downloads/novels'.
  2. If relying on the default, pass undefined (not '' or null with an empty fallback) so the fallback './pixiv-downloads/novels' applies.
  3. Strip or reject NUL bytes from user-supplied paths before calling the API.

Example fix

// before
const dir = process.env.OUTPUT ?? ''; // '' bypasses fallback
normalizePixivOutputRoot(dir);
// after
const dir = process.env.OUTPUT || undefined;
normalizePixivOutputRoot(dir, './pixiv-downloads/novels');
Defensive patterns

Strategy: validation

Validate before calling

const dir = output ?? fallback;
if (typeof dir !== 'string' || dir.length === 0 || dir.includes('\0')) {
  throw new TypeError('output must be a non-empty path string without NUL bytes');
}

Type guard

function isUsablePath(v) {
  return typeof v === 'string' && v.length > 0 && !v.includes('\0');
}

Try / catch

try {
  await downloadNovel(id, { output });
} catch (e) {
  if (/output must be a non-empty directory path/.test(e.message)) {
    console.error('Set a valid --output directory, e.g. ./pixiv-downloads/novels');
    process.exitCode = 2;
  } else throw e;
}

Prevention

When it happens

Trigger: Calling normalizePixivOutputRoot(''), normalizePixivOutputRoot(null, ''), or a path containing an embedded NUL character such as 'out\0dir'; also when both value and fallback are empty/undefined.

Common situations: Empty OUTPUT env var or shell option (-o "") that overrides the fallback; NUL bytes injected in a path from unsanitized user input or a corrupted config.

Related errors


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