jackwener/OpenCLI · error · ArgumentError

Receipt file cannot be read: ${receipt}

Error message

Receipt file cannot be read: ${receipt}

What it means

parseReceiptPath resolves the receipt argument to an absolute path and stats it before any work starts. If fs.statSync throws (e.g. permission denied, path on broken symlink loop, I/O error) the function wraps the failure in this ArgumentError and attaches the OS error message as the detail. It is distinct from the non-existence error, which is raised separately when stat returns no entry.

Source

Thrown at clis/mercury/utils.js:41

    const value = kwargs[name];
    if (typeof value === 'boolean') return value;
    if (typeof value === 'string') {
        const normalized = value.trim().toLowerCase();
        if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
        if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
        throw new ArgumentError(`Boolean argument ${name} must be true or false`);
    }
    return fallback;
}

function parseReceiptPath(kwargs) {
    const receipt = path.resolve(requireString(kwargs, 'receipt'));
    let stat;
    try {
        stat = fs.statSync(receipt, { throwIfNoEntry: false });
    }
    catch (error) {
        throw new ArgumentError(`Receipt file cannot be read: ${receipt}`, error instanceof Error ? error.message : undefined);
    }
    if (!stat || !stat.isFile()) {
        throw new ArgumentError(`Receipt file does not exist: ${receipt}`);
    }
    return receipt;
}

function parseAmount(kwargs) {
    const amount = requireString(kwargs, 'amount').replace(/,/g, '');
    if (!/^\d+(\.\d{1,2})?$/.test(amount) || Number(amount) <= 0) {
        throw new ArgumentError(`Amount must be a positive number with up to two decimals: ${amount}`);
    }
    return amount;
}

function parseCurrency(kwargs) {
    const currency = optionalString(kwargs, 'currency', 'CNY').toUpperCase();
    if (!/^[A-Z]{3}$/.test(currency)) {

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Verify the file exists and is reachable: run ls -l on the exact path (after resolving symlinks) as the same user running the command.
  2. Fix permissions on the file and each parent directory (chmod/chown or move the file to an accessible location).
  3. Check the argument for stray whitespace, quotes, or a dangling symlink; re-supply a clean absolute path.
  4. Inspect the attached cause message (the error detail) — it contains the underlying OS reason (EACCES, ELOOP, etc.) to target the fix.

Example fix

// before
node mercury.js reimburse --receipt /mnt/usb/receipt.pdf   # unmounted drive
// after
node mercury.js reimburse --receipt /home/alice/receipt.pdf  # local readable path
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
function canStatReceipt(p) {
  try { fs.accessSync(p, fs.constants.R_OK); return true; } catch { return false; }
}

Type guard

function isReadableFile(p) {
  try { return fs.statSync(p, { throwIfNoEntry: false })?.isFile() ?? false; } catch { return false; }
}

Try / catch

try {
  await reimburse({ receipt });
} catch (e) {
  if (e instanceof ArgumentError && e.message.startsWith('Receipt file cannot be read')) {
    console.error(`Cannot access receipt (${e.message}). Check permissions/symlinks: ${e.cause ?? ''}`);
    process.exitCode = 2;
  } else throw e;
}

Prevention

When it happens

Trigger: Calling normalizeReimbursementInput with a receipt path that points to a file the process cannot stat: parent directory not searchable, a dangling symlink (stat fails on some platforms), a path with invalid characters, or a device/disk error during stat.

Common situations: Running the CLI under a user without read access to ~/Downloads or a mounted share; receipt on an unmounted USB/network drive; typo'd path through a symlink pointing at a removed target; Windows paths with stray quotes from copy-paste.

Related errors


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