Skip to content
Open-source libraryLive

Keeps the machine awake while long jobs run.

A sleep and display-sleep inhibitor for Node scripts — backups, renders, batch imports. It spawns the OS’s own mechanism directly, with no native addons.

  • 0

    runtime dependencies

  • 3

    supported OS platforms

  • 2

    independent sleep axes

import { keepAwake } from "pervigil"; const lock = await keepAwake({ system: true, description: "nightly backup" });try {  await runLongJob();} finally {  await lock.release();}

At a glance

Type
My own open-source library
Scope
Everything: the three OS drivers (macOS, Linux, Windows), the controller and declarative supervisor layers, the test suite, and the npm release pipeline.
Status
Live — 0.6.0 on npm · as of 24 Sep 2026
Timeline

About 7 weeks to the first release · Jun 2026

Extracted from another of my projects, then grew more complex during the move

Latest 0.6.0 on npm, 26 Jun 2026

Platforms
Node.js 20+, macOS, Linux, Windows
Try it
npm install pervigil
Stack
TypeScript, Node.js

The challenge

Node has no cross-platform API for host sleep: macOS, Linux and Windows each expose a different, OS-specific mechanism, and the obvious way to reach all three is a native addon compiled per platform. A wake lock also has to survive the real world — the OS primitive can die mid-job, the process can crash before releasing it, and a container or unsupported platform has no primitive to hold at all. None of that is allowed to make the caller believe the host is awake when it silently isn’t.

  • Three unrelated OS mechanisms — caffeinate, systemd-inhibit, SetThreadExecutionState — with no shared Node API, and the obvious fix (a native addon) breaks on Alpine/musl, ARM, Docker and CI.
  • The OS primitive is a spawned child process that can die mid-job and has to be noticed and re-engaged.
  • A silent no-op must never be mistaken for a real assertion — available, engaged and active have to stay three different questions.
  • A forgotten release() must never leak an orphaned process, but the cleanup can’t install a SIGTERM handler without fighting the host application’s own shutdown logic.
  • Independent callers can each request the same axis at once; reconciling them onto one OS call has to be race-free.

The constraints

Whatever else changed release to release, three limits never moved: no build step for consumers, no throwing by default, and never taking over the host process’s own exit handling.

  • Zero runtime dependencies and no native addons — only the OS’s own binary is spawned, so install needs no compiler.
  • Never throw by default, even with no OS primitive available — a job keeps running unless it opts into strict.
  • Never install a SIGINT/SIGTERM handler by default — it would override the host process’s own signal handling.
  • Never keep the Node.js event loop alive — every internal timer is created unref()’d.
  • Node 20+ only — required for the explicit resource management ("await using") the API leans on.

The solution

I built pervigil around one pattern reused on every platform: spawn and supervise the OS’s own long-lived process — caffeinate, systemd-inhibit, or a PowerShell script calling SetThreadExecutionState — instead of a native addon. A small core, a driver interface plus an engine that reconciles named reasons onto two axes, is then reused by three public entry points: a one-shot keepAwake, a multi-reason wakeLock, and a declarative supervise layer.

  • One pattern for every OS: supervise a long-lived child process

    A native addon would need a compiled binary per platform. Spawning and supervising the OS’s own CLI — caffeinate, systemd-inhibit, or a PowerShell script — keeps the same zero-native-dependency promise on macOS, Linux and Windows.

  • Two independent axes, not one flag

    System sleep and display sleep are different OS assertions. A backup job only needs the system axis; a live preview needs the display axis held too, without forcing the other one on.

  • Fail-safe no-op by default; strict opts into fail-fast

    A job should keep running even where no primitive exists, such as a container or an unsupported OS. strict exists for the opposite case, where running unawake is worse than not running.

  • Auto-release on process exit became the default (v0.4.0)

    A forgotten shutdown() used to leak an orphaned OS process. One shared process "exit" handler now releases every lock automatically, without a signal listener that could interfere with the caller’s own Ctrl-C handling.

  • A declarative supervisor layered on the controller, not a replacement

    Daemons juggling many overlapping reasons shouldn’t have to hand-track lifetimes. supervise() lets each lock declare an active predicate or an eviction trigger and reconciles them through the same wakeLock underneath.

Architecture

keepAwake()

One-shot lock, scoped release

wakeLock()

Multi-reason controller

supervise()

Self-managing, declarative locks

detectDriver()

Picks a driver per OS

WakeLockEngine

Reconciles reasons → axes

macOS driver

caffeinate(1)

Linux driver

systemd-inhibit, sysfs fallback

Windows driver

PowerShell + SetThreadExecutionState

Noop driver

Fail-safe no-op

Exit cleanup

Auto-releases on process exit

Status & metrics

Events + Prometheus text

A caller picks keepAwake, wakeLock or supervise; the engine detects the right OS driver and reconciles every named reason onto two axes, while every driver reports the same status for events, metrics and exit cleanup.

Built to hand over

Pervigil’s repository is public, so I can point at a handover instead of describing one — every item below is a real link into it, not a summary of it.

  • Source and licence

    MIT-licensed — 19 source files and 12 test files in src/, all one author.

    View (opens in a new tab)
  • Roadmap and security policy

    ROADMAP.md tracks shipped work against what is still open; SECURITY.md covers private vulnerability disclosure.

    View (opens in a new tab)
  • Test suite

    12 Vitest files, run in CI across Linux, macOS and Windows on Node 20 and 22.

    View (opens in a new tab)
  • Release pipeline

    Tag-triggered workflow — reruns the full CI matrix, then publishes to npm over OIDC trusted publishing with provenance and cuts a GitHub release.

    View (opens in a new tab)

Results

pervigil is published on npm and MIT-licensed on GitHub; every push runs the full test suite on Linux, macOS and Windows across Node 20 and 22.

  • MIT

    open-source licence

What’s next

The documented next steps: integration tests that assert directly against the OS (pmset -g assertions on macOS, systemd-inhibit --list on Linux) instead of only the mock driver, and a further OpenTelemetry example alongside the existing Prometheus adapter.

Have a similar project in mind?

Tell me what you’re working on. I’ll tell you whether I’m the right person for it.

Discuss a project

No pressure. Just a conversation.