Skip to content
oleks crane
Open-source libraryLive

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
Try it
npm install pervigil
Stack
TypeScriptNode.jstsupVitestESLintpnpmGitHub Actions

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.

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.

  • 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?

I’ll build it and hand it over: the code, the docs and the accounts are yours.

Discuss a project