Keeps the machine awake while long jobs run.
A cross-platform sleep and display-sleep inhibitor for Node, for scripts that need the host awake until a long job finishes — backups, renders, batch imports. It spawns the OS’s own mechanism directly: zero runtime dependencies, 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
- Source code
- Source on GitHub (opens in a new tab)
- Try it
npm install pervigil- Stack
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
I build — you own
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.
- View (opens in a new tab)
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.
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?
I’ll build it and hand it over: the code, the docs and the accounts are yours.
Discuss a project