What pompelmi is

pompelmi is a Node.js library that provides antivirus file scanning by delegating to ClamAV, the open-source antivirus engine. It exposes a single async function, scan(filePath), which returns a Promise resolving to one of three strings: "Clean", "Malicious", or "ScanError".

The name pompelmi is the Italian plural of pompelmo (grapefruit). It was chosen because grapefruits are bright, sharp, and slightly bitter — much like a good security check.

Why it exists

Most Node.js antivirus integrations either require running a persistent ClamAV daemon (clamd), parse raw stdout from clamscan, or wrap a native C extension. pompelmi takes the simplest possible path:

  • Spawn clamscan as a child process.
  • Read the exit code when it finishes.
  • Map that code to a string constant and resolve the Promise.

No daemon. No stdout parsing. No native bindings. The result is a library that is trivial to install, audit, and reason about.

How it works

When you call pompelmi.scan('/path/to/file'), the following happens:

  1. The file path is checked with fs.existsSync. If missing, the Promise rejects immediately.
  2. cross-spawn launches clamscan --no-summary <filePath> as a child process.
  3. pompelmi listens for the close event on the child process.
  4. The exit code is looked up in the SCAN_RESULTS map: 0 → "Clean", 1 → "Malicious", 2 → "ScanError".
  5. The Promise resolves with the matching string, or rejects if the code is unrecognised.

The --no-summary flag suppresses ClamAV's final statistics output, keeping stdout clean. pompelmi never reads stdout at all; only the exit code matters.

Architecture

The data flow from a calling application to a scan result:

your code
    |
    | pompelmi.scan(filePath)
    v
ClamAVScanner.js
    |
    | cross-spawn('clamscan', ['--no-summary', filePath])
    v
clamscan (system binary)
    |
    | exit code: 0 / 1 / 2
    v
SCAN_RESULTS map  (config.js)
    |
    | "Clean" / "Malicious" / "ScanError"
    v
Promise resolved → your code

The installer and updater helpers follow the same pattern: they spawn the appropriate system package manager or freshclam and report success or failure via the exit code.

Modules

File Exports Purpose
index.js pompelmi Public entry point. Re-exports { scan }.
ClamAVScanner.js scan(filePath) Spawns clamscan, maps exit code, returns Promise.
ClamAVInstaller.js ClamAVInstaller() Installs ClamAV using the platform's package manager.
ClamAVDatabaseUpdater.js updateClamAVDatabase() Runs freshclam to refresh virus definitions.
InstallerCommand.js getInstallerCommand(), getUpdaterCommand() Returns the correct [binary, args] pair for the current platform.
config.js INSTALLER_COMMANDS, UPDATER_COMMANDS, DB_PATHS, SCAN_RESULTS Frozen config object. Single source of truth for all commands and result strings.
constants.js PLATFORM Exports process.platform so it can be mocked in tests.

Dependencies

Package Version Why
cross-spawn ^7.0.6 Node's built-in child_process.spawn has edge cases on Windows (PATHEXT, quoting). cross-spawn handles these transparently without changing the API.

There are no other runtime dependencies.

Dev dependencies

Package Purpose
eslint, @eslint/js, globals Static analysis. Configured in eslint.config.mjs.

Design decisions

Why not use the clamd daemon?

The ClamAV daemon (clamd) provides faster repeated scanning because it keeps virus definitions in memory. However, it requires managing a background service, a socket or TCP connection, and additional configuration. pompelmi prioritises simplicity: spawn a process, get a result, done. For high-throughput scanning pipelines, switching to a clamd-based approach is reasonable, but that is outside pompelmi's scope.

Why return a string instead of a boolean or number?

A boolean (true/false) would collapse the three-state result into two, losing the "ScanError" case which represents a distinct and actionable condition: the scan failed and the file's safety is unknown. A number would require callers to remember what 2 means. A string is self-documenting and safe to log, serialize, and switch on.

Why freeze the config object?

config.js exports a frozen object. This prevents accidental mutation of shared configuration at runtime. Because config values are looked up on every scan, a mutation bug could silently change behaviour across all scans in a process.

Why cross-spawn instead of execa or shelljs?

cross-spawn has a minimal surface area, is widely used, and does exactly one thing. Heavier alternatives like execa add promise wrappers and output parsing on top of what pompelmi needs, which is just an exit code.

Limitations

  • Requires ClamAV on the host. pompelmi does not bundle or download ClamAV. The clamscan binary must be in PATH.
  • Virus definitions must be kept up to date manually. ClamAV's detection quality depends on fresh definitions. Run freshclam regularly or set up a cron job.
  • Scanning speed depends on file size. Each call spawns a new clamscan process, which loads definitions from disk every time. For bulk scanning, consider batching calls or switching to the clamd socket.
  • No directory scanning. scan() accepts a single file path. ClamAV supports recursive directory scanning, but pompelmi does not currently expose that option.
  • No streaming support. The file must exist on disk. Scanning a stream or buffer directly is not supported.