npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

uncontainerizable

v0.1.3

Published

Graceful process lifecycle for programs that can't be containerized

Downloads

82

Readme

uncontainerizable

Graceful process lifecycle for programs that can't be put in real containers.

A supervisor for the apps you can't put in Docker: browsers, GUI apps, and anything that needs the user's window server, keychain, or display. Built on a pure-Rust core with Node bindings via napi-rs; the published binary is prebuilt for every supported target.

If the program can run in a real sandbox (namespaces, seccomp, landlock), use a real container runtime. uncontainerizable is for everything else.

Features

  • Staged quit ladder. Each platform escalates from its polite quit channel to a guaranteed kill, with per-stage timeouts and skippable stages.
  • Tree-aware teardown. Helper processes get reaped alongside the root; the container is "empty" only when no member remains.
  • Identity-based preemption. Spawning with an identity preempts earlier matching instances. Linux and Windows are identity-scoped; macOS .app launches use bundle-scoped preemption on the Launch Services path.
  • Adapter hooks. Per-app lifecycle callbacks suppress "didn't shut down correctly" dialogs after force-kill.
  • Infallible destroy. destroy() aggregates errors into the result so finally blocks never throw.
  • Async first. Promise-based API, Tokio-backed core.

Platform support

| Platform | Preemption primitive | Quit ladder | | ----------------- | -------------------- | ------------------------------------------ | | Linux (x64/arm64) | cgroup v2 | SIGTERMSIGKILL (race-free via freeze) | | macOS (x64/arm64) | argv[0] tag scan / bundle-exec ps scan | aevt/quitSIGTERMSIGKILL | | Windows (x64/arm64) | named Job Object | WM_CLOSETerminateJobObject |

Linux musl is shipped via cargo-zigbuild. Identity strings are namespaced by an app-level prefix (conventionally reverse-DNS) so libraries using uncontainerizable cannot collide.

On macOS, direct-exec launches use argv[0] tag scanning. Launch Services .app launches instead match by bundle executable path via ps comm=, so supplying identity there kills any running instance of that bundle before relaunch, regardless of which identity started it. Launch Services therefore does not support keeping two instances of the same .app alive concurrently through this route. If you need that for an app bundle, first make sure the app itself supports concurrent instances, then pass the inner executable path (Foo.app/Contents/MacOS/Foo) so the launch goes through direct-exec instead of Launch Services.

Installation

npm install uncontainerizable
# or
pnpm add uncontainerizable
# or
yarn add uncontainerizable

The package pulls in @uncontainerizable/native, which resolves to a prebuilt .node binary for the current platform. No native toolchain is needed at install time.

[!IMPORTANT] Node.js ≥ 24 is required (current LTS). The package ships ESM only.

Quick start

import { App, defaultAdapters } from "uncontainerizable";

const app = new App("com.example.my-supervisor");

const container = await app.contain("chromium", {
  args: ["--user-data-dir=/tmp/browser-profile"],
  identity: "browser-main", // preempts any previous "browser-main"
  adapters: [...defaultAdapters],
});

// ... later, when you want to shut it down cleanly:
const result = await container.destroy();

if (result.errors.length > 0) {
  console.warn("teardown surfaced recoverable errors:", result.errors);
}

console.log(`exited at ${result.quit.exitedAtStage}`);

A second call to app.contain(..., { identity: "browser-main" }) will kill the running instance before launching the new one. On macOS .app launches, the same option acts as a bundle-scoped clean-slate switch and clears any running instance of that bundle. Omit identity to skip preemption entirely. If you need concurrent instances of a bundled app on macOS, pass the executable inside the bundle rather than the .app directory, and only do that if the app itself supports multiple instances.

API

new App(prefix)

Namespaced handle for spawning contained processes. The prefix (conventionally reverse-DNS, e.g. "com.example.my-supervisor") namespaces identity strings so unrelated libraries can't collide.

Throws INVALID_IDENTITY if prefix contains characters outside [A-Za-z0-9._:-].

app.contain(command, options?) → Promise<Container>

Spawns a contained process. Options:

| Field | Type | Notes | | ----------------- | ---------------- | ---------------------------------------------------------------- | | args | string[] | Command-line arguments. | | env | Record<…> | Environment overrides. | | cwd | string | Working directory. | | identity | string | Enables preemption; macOS .app launches match by bundle, other routes by identity. Use the inner executable path, not the .app path, if the app supports concurrent instances and you need more than one at once. | | adapters | Adapter[] | Per-app lifecycle hooks. | | darwinTagArgv0 | boolean | macOS direct-exec only; set false if the managed program misreads argv[0] (ignored for .app bundle launches). |

container.quit(options?) → Promise<QuitResult>

Runs the staged quit ladder without releasing platform resources. Use when you want to wait for the process to drain but keep the container handle alive.

container.destroy(options?) → Promise<DestroyResult>

Runs the quit ladder and releases platform resources. Always resolves: recoverable errors appear in result.errors, never as a thrown exception.

coreVersion() → string

Returns the Rust core's version string. Handy for logging and support.

Built-in adapters

Adapters match by probe (bundle ID, executable path, platform). Their clearCrashState hook runs after a terminal-stage teardown to suppress restart dialogs.

| Adapter | Purpose | | --------------- | -------------------------------------------------------------------- | | appkit | Deletes AppKit's "Saved Application State" directory on macOS. | | crashReporter | Clears per-app entries from macOS's user-level CrashReporter archive.| | chromium | Matches Chrome/Chromium/Brave/Edge. Crash-state cleanup is stubbed. | | firefox | Matches Firefox. Crash-state cleanup is stubbed. |

Import individually or as a bundle:

import {
  appkit,
  chromium,
  crashReporter,
  defaultAdapters,
  firefox,
} from "uncontainerizable";

Custom adapters

An adapter is any object matching the Adapter shape. Every hook except name and matches is optional; unimplemented hooks are skipped. Hooks may be sync or async; the wrapper normalizes both forms before crossing the napi boundary.

import type { Adapter } from "uncontainerizable";

const logger: Adapter = {
  name: "logger",
  matches: () => true,
  beforeStage(probe, stageName) {
    console.log(`[${probe.pid}] entering stage ${stageName}`);
  },
  afterStage(_probe, result) {
    if (result.exited) {
      console.log(`drained at ${result.stageName}`);
    }
  },
};

Hook surface:

  • beforeQuit(probe): before the ladder starts.
  • beforeStage(probe, stageName) / afterStage(probe, result): around each stage.
  • afterQuit(probe, result): after the ladder ends, terminal or not.
  • clearCrashState(probe): only after a terminal-stage teardown.

[!TIP] Adapter hooks are advisory: errors are collected into QuitResult.adapterErrors and never abort the quit ladder. A misbehaving adapter cannot prevent teardown.

TypeScript

All public types are exported from the package root:

import type {
  Adapter,
  ContainOptions,
  Container,
  DestroyOptions,
  DestroyResult,
  Probe,
  QuitOptions,
  QuitResult,
  StageResult,
  SupportedPlatform,
} from "uncontainerizable";

License

MIT