uncontainerizable 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +209 -0
  2. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # uncontainerizable
2
+
3
+ > Graceful process lifecycle for programs that can't be put in real containers.
4
+
5
+ A supervisor for the apps you can't put in Docker: browsers, GUI apps,
6
+ and anything that needs the user's window server, keychain, or display.
7
+ Built on a pure-Rust core with Node bindings via
8
+ [napi-rs](https://napi.rs); the published binary is prebuilt for every
9
+ supported target.
10
+
11
+ If the program can run in a real sandbox — namespaces, seccomp, landlock
12
+ — use a real container runtime. `uncontainerizable` is for everything else.
13
+
14
+ ## Features
15
+
16
+ - **Staged quit ladder.** Each platform escalates from its polite quit
17
+ channel to a guaranteed kill, with per-stage timeouts and skippable
18
+ stages.
19
+ - **Tree-aware teardown.** Helper processes get reaped alongside the root;
20
+ the container is "empty" only when no member remains.
21
+ - **Identity-based singleton.** At most one container per identity is
22
+ alive at a time; spawning preempts any predecessor using kernel
23
+ primitives on Linux and Windows.
24
+ - **Adapter hooks.** Per-app lifecycle callbacks suppress "didn't shut
25
+ down correctly" dialogs after force-kill.
26
+ - **Infallible destroy.** `destroy()` aggregates errors into the result
27
+ so `finally` blocks never throw.
28
+ - **Async first.** `Promise`-based API, Tokio-backed core.
29
+
30
+ ## Platform support
31
+
32
+ | Platform | Preemption primitive | Quit ladder |
33
+ | ----------------- | -------------------- | ------------------------------------------ |
34
+ | Linux (x64/arm64) | cgroup v2 | `SIGTERM` → `SIGKILL` (race-free via freeze) |
35
+ | macOS (x64/arm64) | `argv[0]` tag scan | `aevt/quit` → `SIGTERM` → `SIGKILL` |
36
+ | Windows (x64/arm64) | named Job Object | `WM_CLOSE` → `TerminateJobObject` |
37
+
38
+ Linux musl is shipped via `cargo-zigbuild`. Identity strings are
39
+ namespaced by an app-level prefix (conventionally reverse-DNS) so
40
+ libraries using `uncontainerizable` cannot collide.
41
+
42
+ ## Installation
43
+
44
+ ```sh
45
+ npm install uncontainerizable
46
+ # or
47
+ pnpm add uncontainerizable
48
+ # or
49
+ yarn add uncontainerizable
50
+ ```
51
+
52
+ The package pulls in `@uncontainerizable/native`, which resolves to a
53
+ prebuilt `.node` binary for the current platform. No native toolchain
54
+ is needed at install time.
55
+
56
+ > [!IMPORTANT]
57
+ > Node.js ≥ 24 is required (current LTS). The package ships ESM only.
58
+
59
+ ## Quick start
60
+
61
+ ```ts
62
+ import { App, defaultAdapters } from "uncontainerizable";
63
+
64
+ const app = new App("com.example.my-supervisor");
65
+
66
+ const container = await app.contain("chromium", {
67
+ args: ["--user-data-dir=/tmp/browser-profile"],
68
+ identity: "browser-main", // preempts any previous "browser-main"
69
+ adapters: [...defaultAdapters],
70
+ });
71
+
72
+ // ... later, when you want to shut it down cleanly:
73
+ const result = await container.destroy();
74
+
75
+ if (result.errors.length > 0) {
76
+ console.warn("teardown surfaced recoverable errors:", result.errors);
77
+ }
78
+
79
+ console.log(`exited at ${result.quit.exitedAtStage}`);
80
+ ```
81
+
82
+ A second call to `app.contain(..., { identity: "browser-main" })` will
83
+ kill the running instance before launching the new one. Omit `identity`
84
+ to skip preemption entirely.
85
+
86
+ ## API
87
+
88
+ ### `new App(prefix)`
89
+
90
+ Namespaced handle for spawning contained processes. The `prefix`
91
+ (conventionally reverse-DNS, e.g. `"com.example.my-supervisor"`)
92
+ namespaces identity strings so unrelated libraries can't collide.
93
+
94
+ Throws `INVALID_IDENTITY` if `prefix` contains characters outside
95
+ `[A-Za-z0-9._:-]`.
96
+
97
+ ### `app.contain(command, options?) → Promise<Container>`
98
+
99
+ Spawns a contained process. Options:
100
+
101
+ | Field | Type | Notes |
102
+ | ----------------- | ---------------- | ---------------------------------------------------------------- |
103
+ | `args` | `string[]` | Command-line arguments. |
104
+ | `env` | `Record<…>` | Environment overrides. |
105
+ | `cwd` | `string` | Working directory. |
106
+ | `identity` | `string` | Enables singleton enforcement; prior instance is killed first. |
107
+ | `adapters` | `Adapter[]` | Per-app lifecycle hooks. |
108
+ | `darwinTagArgv0` | `boolean` | macOS only; set `false` if the managed program misreads argv[0]. |
109
+
110
+ ### `container.quit(options?) → Promise<QuitResult>`
111
+
112
+ Runs the staged quit ladder without releasing platform resources. Use
113
+ when you want to wait for the process to drain but keep the container
114
+ handle alive.
115
+
116
+ ### `container.destroy(options?) → Promise<DestroyResult>`
117
+
118
+ Runs the quit ladder and releases platform resources. Always resolves:
119
+ recoverable errors appear in `result.errors`, never as a thrown
120
+ exception.
121
+
122
+ ### `coreVersion() → string`
123
+
124
+ Returns the Rust core's version string — handy for logging and support.
125
+
126
+ ## Built-in adapters
127
+
128
+ Adapters match by probe (bundle ID, executable path, platform). Their
129
+ `clearCrashState` hook runs after a terminal-stage teardown to suppress
130
+ restart dialogs.
131
+
132
+ | Adapter | Purpose |
133
+ | --------------- | -------------------------------------------------------------------- |
134
+ | `appkit` | Deletes AppKit's "Saved Application State" directory on macOS. |
135
+ | `crashReporter` | Clears per-app entries from macOS's user-level CrashReporter archive.|
136
+ | `chromium` | Matches Chrome/Chromium/Brave/Edge. Crash-state cleanup is stubbed. |
137
+ | `firefox` | Matches Firefox. Crash-state cleanup is stubbed. |
138
+
139
+ Import individually or as a bundle:
140
+
141
+ ```ts
142
+ import {
143
+ appkit,
144
+ chromium,
145
+ crashReporter,
146
+ defaultAdapters,
147
+ firefox,
148
+ } from "uncontainerizable";
149
+ ```
150
+
151
+ ## Custom adapters
152
+
153
+ An adapter is any object matching the `Adapter` shape. Every hook except
154
+ `name` and `matches` is optional; unimplemented hooks are skipped. Hooks
155
+ may be sync or async — the wrapper normalizes both forms before crossing
156
+ the napi boundary.
157
+
158
+ ```ts
159
+ import type { Adapter } from "uncontainerizable";
160
+
161
+ const logger: Adapter = {
162
+ name: "logger",
163
+ matches: () => true,
164
+ beforeStage(probe, stageName) {
165
+ console.log(`[${probe.pid}] entering stage ${stageName}`);
166
+ },
167
+ afterStage(_probe, result) {
168
+ if (result.exited) {
169
+ console.log(`drained at ${result.stageName}`);
170
+ }
171
+ },
172
+ };
173
+ ```
174
+
175
+ Hook surface:
176
+
177
+ - `beforeQuit(probe)` — before the ladder starts.
178
+ - `beforeStage(probe, stageName)` / `afterStage(probe, result)` — around
179
+ each stage.
180
+ - `afterQuit(probe, result)` — after the ladder ends, terminal or not.
181
+ - `clearCrashState(probe)` — only after a terminal-stage teardown.
182
+
183
+ > [!TIP]
184
+ > Adapter hooks are **advisory**: errors are collected into
185
+ > `QuitResult.adapterErrors` and never abort the quit ladder. A
186
+ > misbehaving adapter cannot prevent teardown.
187
+
188
+ ## TypeScript
189
+
190
+ All public types are exported from the package root:
191
+
192
+ ```ts
193
+ import type {
194
+ Adapter,
195
+ ContainOptions,
196
+ Container,
197
+ DestroyOptions,
198
+ DestroyResult,
199
+ Probe,
200
+ QuitOptions,
201
+ QuitResult,
202
+ StageResult,
203
+ SupportedPlatform,
204
+ } from "uncontainerizable";
205
+ ```
206
+
207
+ ## License
208
+
209
+ [MIT](https://github.com/inakineitor/uncontainerizable/blob/main/LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uncontainerizable",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Graceful process lifecycle for programs that can't be containerized",
5
5
  "keywords": [
6
6
  "lifecycle",
@@ -41,7 +41,7 @@
41
41
  "provenance": true
42
42
  },
43
43
  "dependencies": {
44
- "@uncontainerizable/native": "0.1.0"
44
+ "@uncontainerizable/native": "0.1.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^24",