uncontainerizable 0.0.3 → 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.
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)
@@ -1 +1,2 @@
1
- export { };
1
+ import { a as appkit, i as chromium, n as firefox, r as crashReporter, t as defaultAdapters } from "../index-B1y4yb1N.mjs";
2
+ export { appkit, chromium, crashReporter, defaultAdapters, firefox };
@@ -1 +1,2 @@
1
- export { };
1
+ import { a as appkit, i as chromium, n as firefox, r as crashReporter, t as defaultAdapters } from "../adapters-BYLFpYWC.mjs";
2
+ export { appkit, chromium, crashReporter, defaultAdapters, firefox };
@@ -0,0 +1,157 @@
1
+ import { readdir, rm, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ //#region src/adapters/appkit.ts
5
+ /**
6
+ * Deletes the AppKit "Saved Application State" directory for the managed
7
+ * app after a terminal-stage destroy.
8
+ *
9
+ * macOS's AppKit persists a per-app snapshot of open windows and the
10
+ * "should reopen windows on next launch" hint at
11
+ * `~/Library/Saved Application State/<bundleId>.savedState/`. When an
12
+ * app is force-killed (our SIGTERM/SIGKILL ladder), AppKit interprets
13
+ * the missing clean-shutdown marker as a crash and shows the
14
+ * "Reopen windows?" dialog next launch. Wiping the directory suppresses
15
+ * the dialog.
16
+ *
17
+ * Only matches on darwin probes that carry a resolved `bundleId`.
18
+ * Programs spawned without bundle-ID resolution (anything outside
19
+ * `lsappinfo`'s knowledge of launched apps, for example `sleep` or a
20
+ * manually-compiled binary) silently skip.
21
+ */
22
+ const appkit = {
23
+ name: "appkit",
24
+ matches(probe) {
25
+ return probe.platform === "darwin" && typeof probe.bundleId === "string";
26
+ },
27
+ async clearCrashState(probe) {
28
+ if (!probe.bundleId) return;
29
+ await rm(join(homedir(), "Library", "Saved Application State", `${probe.bundleId}.savedState`), {
30
+ recursive: true,
31
+ force: true
32
+ });
33
+ }
34
+ };
35
+ //#endregion
36
+ //#region src/adapters/chromium.ts
37
+ const CHROMIUM_BASENAMES = [
38
+ "chrome",
39
+ "chromium",
40
+ "Chromium",
41
+ "Google Chrome",
42
+ "brave",
43
+ "Brave Browser",
44
+ "msedge",
45
+ "Microsoft Edge"
46
+ ];
47
+ /**
48
+ * Stub adapter for Chromium-family browsers.
49
+ *
50
+ * Matches by executable basename (case-sensitive, since bundle exe names
51
+ * are preserved verbatim). The real `clearCrashState` implementation
52
+ * edits the "Last Exit Type" key in the browser's `Preferences` JSON so
53
+ * the next launch does not prompt the user to restore a crashed
54
+ * session. That implementation needs per-OS profile-dir detection and
55
+ * careful JSON editing; it ships in a follow-up.
56
+ *
57
+ * Until then `clearCrashState` emits a warning and returns. The adapter
58
+ * still matches so callers wiring it in can see the "didn't run" signal
59
+ * and swap in a real cleanup when the full version ships.
60
+ */
61
+ const chromium = {
62
+ name: "chromium",
63
+ matches(probe) {
64
+ const exe = probe.executablePath ? basename(probe.executablePath) : void 0;
65
+ return exe ? CHROMIUM_BASENAMES.includes(exe) : false;
66
+ },
67
+ clearCrashState(_probe) {
68
+ console.warn("uncontainerizable: chromium.clearCrashState is a stub; the next launch may show a 'didn't shut down correctly' dialog. Track the real implementation in a follow-up release.");
69
+ }
70
+ };
71
+ //#endregion
72
+ //#region src/adapters/crash-reporter.ts
73
+ /**
74
+ * Deletes per-app crash reports from macOS's user-level CrashReporter
75
+ * archive after a terminal-stage destroy.
76
+ *
77
+ * When our SIGTERM/SIGKILL ladder kills an app, macOS's `ReportCrash`
78
+ * daemon writes an `.ips` entry under
79
+ * `~/Library/Logs/DiagnosticReports/` named like
80
+ * `<AppName>_<timestamp>_<host>.ips`. The next time the user opens the
81
+ * Console app (or the crash-reporter dialog appears) those entries
82
+ * show up as "<App> quit unexpectedly" even though the quit was
83
+ * deliberate.
84
+ *
85
+ * This adapter matches by the basename of `Probe.executablePath` and
86
+ * deletes every report whose filename starts with that basename,
87
+ * followed by an underscore. We only touch files under the user's
88
+ * DiagnosticReports directory; the system-wide `/Library/Logs/...`
89
+ * archive is root-owned and out of scope.
90
+ *
91
+ * Note that this does NOT suppress the modal "quit unexpectedly"
92
+ * dialog that fires immediately when a process crashes: that is
93
+ * displayed before any cleanup hook runs. For that case, let the
94
+ * `apple_event_quit` stage resolve the quit cleanly instead of
95
+ * escalating to SIGTERM/SIGKILL.
96
+ */
97
+ const crashReporter = {
98
+ name: "crashReporter",
99
+ matches(probe) {
100
+ return probe.platform === "darwin" && typeof probe.executablePath === "string";
101
+ },
102
+ async clearCrashState(probe) {
103
+ if (!probe.executablePath) return;
104
+ const appName = basename(probe.executablePath);
105
+ if (!appName) return;
106
+ const dir = join(homedir(), "Library", "Logs", "DiagnosticReports");
107
+ if (!(await stat(dir).catch(() => void 0))?.isDirectory()) return;
108
+ const entries = await readdir(dir);
109
+ const prefix = `${appName}_`;
110
+ const removals = entries.filter((name) => name.startsWith(prefix) && (name.endsWith(".ips") || name.endsWith(".crash"))).map((name) => rm(join(dir, name), { force: true }));
111
+ await Promise.all(removals);
112
+ }
113
+ };
114
+ //#endregion
115
+ //#region src/adapters/firefox.ts
116
+ const FIREFOX_BASENAMES = [
117
+ "firefox",
118
+ "Firefox",
119
+ "firefox-bin"
120
+ ];
121
+ /**
122
+ * Stub adapter for Firefox.
123
+ *
124
+ * Matches by executable basename. The real `clearCrashState` deletes
125
+ * the profile's `sessionstore-backups/recovery.jsonlz4` so the next
126
+ * launch does not offer session restore. Per-OS profile-dir detection
127
+ * and lz4 handling ship in a follow-up.
128
+ *
129
+ * Until then `clearCrashState` emits a warning and returns.
130
+ */
131
+ const firefox = {
132
+ name: "firefox",
133
+ matches(probe) {
134
+ const exe = probe.executablePath ? basename(probe.executablePath) : void 0;
135
+ return exe ? FIREFOX_BASENAMES.includes(exe) : false;
136
+ },
137
+ clearCrashState(_probe) {
138
+ console.warn("uncontainerizable: firefox.clearCrashState is a stub; the next launch may prompt to restore the previous session. Track the real implementation in a follow-up release.");
139
+ }
140
+ };
141
+ //#endregion
142
+ //#region src/adapters/index.ts
143
+ /**
144
+ * The bundle a typical browser-supervising caller wants: every built-in
145
+ * adapter, in an order that doesn't matter (hook invocation is
146
+ * idempotent and non-overlapping across adapters).
147
+ */
148
+ const defaultAdapters = [
149
+ chromium,
150
+ firefox,
151
+ appkit,
152
+ crashReporter
153
+ ];
154
+ //#endregion
155
+ export { appkit as a, chromium as i, firefox as n, crashReporter as r, defaultAdapters as t };
156
+
157
+ //# sourceMappingURL=adapters-BYLFpYWC.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapters-BYLFpYWC.mjs","names":[],"sources":["../src/adapters/appkit.ts","../src/adapters/chromium.ts","../src/adapters/crash-reporter.ts","../src/adapters/firefox.ts","../src/adapters/index.ts"],"sourcesContent":["import { rm } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport type { Adapter, Probe } from \"#/types.js\";\n\n/**\n * Deletes the AppKit \"Saved Application State\" directory for the managed\n * app after a terminal-stage destroy.\n *\n * macOS's AppKit persists a per-app snapshot of open windows and the\n * \"should reopen windows on next launch\" hint at\n * `~/Library/Saved Application State/<bundleId>.savedState/`. When an\n * app is force-killed (our SIGTERM/SIGKILL ladder), AppKit interprets\n * the missing clean-shutdown marker as a crash and shows the\n * \"Reopen windows?\" dialog next launch. Wiping the directory suppresses\n * the dialog.\n *\n * Only matches on darwin probes that carry a resolved `bundleId`.\n * Programs spawned without bundle-ID resolution (anything outside\n * `lsappinfo`'s knowledge of launched apps, for example `sleep` or a\n * manually-compiled binary) silently skip.\n */\nexport const appkit: Adapter = {\n name: \"appkit\",\n\n matches(probe: Probe): boolean {\n return probe.platform === \"darwin\" && typeof probe.bundleId === \"string\";\n },\n\n async clearCrashState(probe: Probe): Promise<void> {\n if (!probe.bundleId) {\n return;\n }\n const dir = join(\n homedir(),\n \"Library\",\n \"Saved Application State\",\n `${probe.bundleId}.savedState`\n );\n await rm(dir, { recursive: true, force: true });\n },\n};\n","import { basename } from \"node:path\";\n\nimport type { Adapter, Probe } from \"#/types.js\";\n\nconst CHROMIUM_BASENAMES = [\n \"chrome\",\n \"chromium\",\n \"Chromium\",\n \"Google Chrome\",\n \"brave\",\n \"Brave Browser\",\n \"msedge\",\n \"Microsoft Edge\",\n];\n\n/**\n * Stub adapter for Chromium-family browsers.\n *\n * Matches by executable basename (case-sensitive, since bundle exe names\n * are preserved verbatim). The real `clearCrashState` implementation\n * edits the \"Last Exit Type\" key in the browser's `Preferences` JSON so\n * the next launch does not prompt the user to restore a crashed\n * session. That implementation needs per-OS profile-dir detection and\n * careful JSON editing; it ships in a follow-up.\n *\n * Until then `clearCrashState` emits a warning and returns. The adapter\n * still matches so callers wiring it in can see the \"didn't run\" signal\n * and swap in a real cleanup when the full version ships.\n */\nexport const chromium: Adapter = {\n name: \"chromium\",\n\n matches(probe: Probe): boolean {\n const exe = probe.executablePath\n ? basename(probe.executablePath)\n : undefined;\n return exe ? CHROMIUM_BASENAMES.includes(exe) : false;\n },\n\n clearCrashState(_probe: Probe): void {\n console.warn(\n \"uncontainerizable: chromium.clearCrashState is a stub; the next launch may show a 'didn't shut down correctly' dialog. Track the real implementation in a follow-up release.\"\n );\n },\n};\n","import { readdir, rm, stat } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, join } from \"node:path\";\n\nimport type { Adapter, Probe } from \"#/types.js\";\n\n/**\n * Deletes per-app crash reports from macOS's user-level CrashReporter\n * archive after a terminal-stage destroy.\n *\n * When our SIGTERM/SIGKILL ladder kills an app, macOS's `ReportCrash`\n * daemon writes an `.ips` entry under\n * `~/Library/Logs/DiagnosticReports/` named like\n * `<AppName>_<timestamp>_<host>.ips`. The next time the user opens the\n * Console app (or the crash-reporter dialog appears) those entries\n * show up as \"<App> quit unexpectedly\" even though the quit was\n * deliberate.\n *\n * This adapter matches by the basename of `Probe.executablePath` and\n * deletes every report whose filename starts with that basename,\n * followed by an underscore. We only touch files under the user's\n * DiagnosticReports directory; the system-wide `/Library/Logs/...`\n * archive is root-owned and out of scope.\n *\n * Note that this does NOT suppress the modal \"quit unexpectedly\"\n * dialog that fires immediately when a process crashes: that is\n * displayed before any cleanup hook runs. For that case, let the\n * `apple_event_quit` stage resolve the quit cleanly instead of\n * escalating to SIGTERM/SIGKILL.\n */\nexport const crashReporter: Adapter = {\n name: \"crashReporter\",\n\n matches(probe: Probe): boolean {\n return (\n probe.platform === \"darwin\" && typeof probe.executablePath === \"string\"\n );\n },\n\n async clearCrashState(probe: Probe): Promise<void> {\n if (!probe.executablePath) {\n return;\n }\n const appName = basename(probe.executablePath);\n if (!appName) {\n return;\n }\n const dir = join(homedir(), \"Library\", \"Logs\", \"DiagnosticReports\");\n // `fs.stat` rejects if the path doesn't exist. A fresh user account\n // may not have a DiagnosticReports directory, so probe first and\n // short-circuit if it's missing or not a directory. Swallowing the\n // reject into `undefined` keeps the happy path straight-line.\n const stats = await stat(dir).catch(() => undefined);\n if (!stats?.isDirectory()) {\n return;\n }\n const entries = await readdir(dir);\n const prefix = `${appName}_`;\n const removals = entries\n .filter(\n (name) =>\n name.startsWith(prefix) &&\n (name.endsWith(\".ips\") || name.endsWith(\".crash\"))\n )\n .map((name) => rm(join(dir, name), { force: true }));\n await Promise.all(removals);\n },\n};\n","import { basename } from \"node:path\";\n\nimport type { Adapter, Probe } from \"#/types.js\";\n\nconst FIREFOX_BASENAMES = [\"firefox\", \"Firefox\", \"firefox-bin\"];\n\n/**\n * Stub adapter for Firefox.\n *\n * Matches by executable basename. The real `clearCrashState` deletes\n * the profile's `sessionstore-backups/recovery.jsonlz4` so the next\n * launch does not offer session restore. Per-OS profile-dir detection\n * and lz4 handling ship in a follow-up.\n *\n * Until then `clearCrashState` emits a warning and returns.\n */\nexport const firefox: Adapter = {\n name: \"firefox\",\n\n matches(probe: Probe): boolean {\n const exe = probe.executablePath\n ? basename(probe.executablePath)\n : undefined;\n return exe ? FIREFOX_BASENAMES.includes(exe) : false;\n },\n\n clearCrashState(_probe: Probe): void {\n console.warn(\n \"uncontainerizable: firefox.clearCrashState is a stub; the next launch may prompt to restore the previous session. Track the real implementation in a follow-up release.\"\n );\n },\n};\n","import { appkit } from \"#/adapters/appkit.js\";\nimport { chromium } from \"#/adapters/chromium.js\";\nimport { crashReporter } from \"#/adapters/crash-reporter.js\";\nimport { firefox } from \"#/adapters/firefox.js\";\nimport type { Adapter } from \"#/types.js\";\n\nexport { appkit } from \"#/adapters/appkit.js\";\nexport { chromium } from \"#/adapters/chromium.js\";\nexport { crashReporter } from \"#/adapters/crash-reporter.js\";\nexport { firefox } from \"#/adapters/firefox.js\";\n\n/**\n * The bundle a typical browser-supervising caller wants: every built-in\n * adapter, in an order that doesn't matter (hook invocation is\n * idempotent and non-overlapping across adapters).\n */\nexport const defaultAdapters: readonly Adapter[] = [\n chromium,\n firefox,\n appkit,\n crashReporter,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,SAAkB;CAC7B,MAAM;CAEN,QAAQ,OAAuB;AAC7B,SAAO,MAAM,aAAa,YAAY,OAAO,MAAM,aAAa;;CAGlE,MAAM,gBAAgB,OAA6B;AACjD,MAAI,CAAC,MAAM,SACT;AAQF,QAAM,GANM,KACV,SAAS,EACT,WACA,2BACA,GAAG,MAAM,SAAS,aACnB,EACa;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;;CAElD;;;ACtCD,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;;;;;;;;AAgBD,MAAa,WAAoB;CAC/B,MAAM;CAEN,QAAQ,OAAuB;EAC7B,MAAM,MAAM,MAAM,iBACd,SAAS,MAAM,eAAe,GAC9B,KAAA;AACJ,SAAO,MAAM,mBAAmB,SAAS,IAAI,GAAG;;CAGlD,gBAAgB,QAAqB;AACnC,UAAQ,KACN,+KACD;;CAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;ACdD,MAAa,gBAAyB;CACpC,MAAM;CAEN,QAAQ,OAAuB;AAC7B,SACE,MAAM,aAAa,YAAY,OAAO,MAAM,mBAAmB;;CAInE,MAAM,gBAAgB,OAA6B;AACjD,MAAI,CAAC,MAAM,eACT;EAEF,MAAM,UAAU,SAAS,MAAM,eAAe;AAC9C,MAAI,CAAC,QACH;EAEF,MAAM,MAAM,KAAK,SAAS,EAAE,WAAW,QAAQ,oBAAoB;AAMnE,MAAI,EADU,MAAM,KAAK,IAAI,CAAC,YAAY,KAAA,EAAU,GACxC,aAAa,CACvB;EAEF,MAAM,UAAU,MAAM,QAAQ,IAAI;EAClC,MAAM,SAAS,GAAG,QAAQ;EAC1B,MAAM,WAAW,QACd,QACE,SACC,KAAK,WAAW,OAAO,KACtB,KAAK,SAAS,OAAO,IAAI,KAAK,SAAS,SAAS,EACpD,CACA,KAAK,SAAS,GAAG,KAAK,KAAK,KAAK,EAAE,EAAE,OAAO,MAAM,CAAC,CAAC;AACtD,QAAM,QAAQ,IAAI,SAAS;;CAE9B;;;AC/DD,MAAM,oBAAoB;CAAC;CAAW;CAAW;CAAc;;;;;;;;;;;AAY/D,MAAa,UAAmB;CAC9B,MAAM;CAEN,QAAQ,OAAuB;EAC7B,MAAM,MAAM,MAAM,iBACd,SAAS,MAAM,eAAe,GAC9B,KAAA;AACJ,SAAO,MAAM,kBAAkB,SAAS,IAAI,GAAG;;CAGjD,gBAAgB,QAAqB;AACnC,UAAQ,KACN,0KACD;;CAEJ;;;;;;;;ACfD,MAAa,kBAAsC;CACjD;CACA;CACA;CACA;CACD"}
@@ -0,0 +1,132 @@
1
+ import { JsContainOptions, JsDestroyOptions, JsDestroyResult, JsProbe, JsQuitOptions, JsQuitResult, JsStageResult, NodeContainer } from "@uncontainerizable/native";
2
+
3
+ //#region src/types.d.ts
4
+ type SupportedPlatform = "linux" | "darwin" | "win32";
5
+ type QuitOptions = JsQuitOptions;
6
+ type DestroyOptions = JsDestroyOptions;
7
+ type QuitResult = JsQuitResult;
8
+ type DestroyResult = JsDestroyResult;
9
+ type StageResult = JsStageResult;
10
+ type Probe = JsProbe;
11
+ /**
12
+ * Platform-agnostic handle for a spawned contained process. Returned by
13
+ * `App.contain`; backed by the napi-generated `NodeContainer` class.
14
+ */
15
+ type Container = NodeContainer;
16
+ /**
17
+ * Per-app lifecycle hook. Each method except `name` and `matches` is
18
+ * optional; unimplemented hooks are skipped by the Rust orchestrator.
19
+ *
20
+ * Every hook may return a value or a Promise; the TypeScript wrapper
21
+ * normalizes both forms to `Promise<_>` before handing the adapter to
22
+ * the napi bridge. Hooks must not throw synchronously: a thrown value
23
+ * is trapped by the bridge and recorded in
24
+ * `QuitResult.adapterErrors`, but ergonomic code should prefer
25
+ * returning a rejected Promise.
26
+ */
27
+ type Adapter = {
28
+ readonly name: string;
29
+ matches(probe: Probe): boolean | Promise<boolean>;
30
+ beforeQuit?(probe: Probe): void | Promise<void>;
31
+ beforeStage?(probe: Probe, stageName: string): void | Promise<void>;
32
+ afterStage?(probe: Probe, result: StageResult): void | Promise<void>;
33
+ afterQuit?(probe: Probe, result: QuitResult): void | Promise<void>;
34
+ clearCrashState?(probe: Probe): void | Promise<void>;
35
+ };
36
+ /**
37
+ * Extends the napi-generated options with a TypeScript-only `adapters`
38
+ * field. The wrapper normalizes each adapter's hook methods to the
39
+ * async signatures the napi bridge expects before crossing the
40
+ * boundary.
41
+ */
42
+ type ContainOptions = Omit<JsContainOptions, "adapters"> & {
43
+ adapters?: Adapter[];
44
+ };
45
+ //#endregion
46
+ //#region src/adapters/appkit.d.ts
47
+ /**
48
+ * Deletes the AppKit "Saved Application State" directory for the managed
49
+ * app after a terminal-stage destroy.
50
+ *
51
+ * macOS's AppKit persists a per-app snapshot of open windows and the
52
+ * "should reopen windows on next launch" hint at
53
+ * `~/Library/Saved Application State/<bundleId>.savedState/`. When an
54
+ * app is force-killed (our SIGTERM/SIGKILL ladder), AppKit interprets
55
+ * the missing clean-shutdown marker as a crash and shows the
56
+ * "Reopen windows?" dialog next launch. Wiping the directory suppresses
57
+ * the dialog.
58
+ *
59
+ * Only matches on darwin probes that carry a resolved `bundleId`.
60
+ * Programs spawned without bundle-ID resolution (anything outside
61
+ * `lsappinfo`'s knowledge of launched apps, for example `sleep` or a
62
+ * manually-compiled binary) silently skip.
63
+ */
64
+ declare const appkit: Adapter;
65
+ //#endregion
66
+ //#region src/adapters/chromium.d.ts
67
+ /**
68
+ * Stub adapter for Chromium-family browsers.
69
+ *
70
+ * Matches by executable basename (case-sensitive, since bundle exe names
71
+ * are preserved verbatim). The real `clearCrashState` implementation
72
+ * edits the "Last Exit Type" key in the browser's `Preferences` JSON so
73
+ * the next launch does not prompt the user to restore a crashed
74
+ * session. That implementation needs per-OS profile-dir detection and
75
+ * careful JSON editing; it ships in a follow-up.
76
+ *
77
+ * Until then `clearCrashState` emits a warning and returns. The adapter
78
+ * still matches so callers wiring it in can see the "didn't run" signal
79
+ * and swap in a real cleanup when the full version ships.
80
+ */
81
+ declare const chromium: Adapter;
82
+ //#endregion
83
+ //#region src/adapters/crash-reporter.d.ts
84
+ /**
85
+ * Deletes per-app crash reports from macOS's user-level CrashReporter
86
+ * archive after a terminal-stage destroy.
87
+ *
88
+ * When our SIGTERM/SIGKILL ladder kills an app, macOS's `ReportCrash`
89
+ * daemon writes an `.ips` entry under
90
+ * `~/Library/Logs/DiagnosticReports/` named like
91
+ * `<AppName>_<timestamp>_<host>.ips`. The next time the user opens the
92
+ * Console app (or the crash-reporter dialog appears) those entries
93
+ * show up as "<App> quit unexpectedly" even though the quit was
94
+ * deliberate.
95
+ *
96
+ * This adapter matches by the basename of `Probe.executablePath` and
97
+ * deletes every report whose filename starts with that basename,
98
+ * followed by an underscore. We only touch files under the user's
99
+ * DiagnosticReports directory; the system-wide `/Library/Logs/...`
100
+ * archive is root-owned and out of scope.
101
+ *
102
+ * Note that this does NOT suppress the modal "quit unexpectedly"
103
+ * dialog that fires immediately when a process crashes: that is
104
+ * displayed before any cleanup hook runs. For that case, let the
105
+ * `apple_event_quit` stage resolve the quit cleanly instead of
106
+ * escalating to SIGTERM/SIGKILL.
107
+ */
108
+ declare const crashReporter: Adapter;
109
+ //#endregion
110
+ //#region src/adapters/firefox.d.ts
111
+ /**
112
+ * Stub adapter for Firefox.
113
+ *
114
+ * Matches by executable basename. The real `clearCrashState` deletes
115
+ * the profile's `sessionstore-backups/recovery.jsonlz4` so the next
116
+ * launch does not offer session restore. Per-OS profile-dir detection
117
+ * and lz4 handling ship in a follow-up.
118
+ *
119
+ * Until then `clearCrashState` emits a warning and returns.
120
+ */
121
+ declare const firefox: Adapter;
122
+ //#endregion
123
+ //#region src/adapters/index.d.ts
124
+ /**
125
+ * The bundle a typical browser-supervising caller wants: every built-in
126
+ * adapter, in an order that doesn't matter (hook invocation is
127
+ * idempotent and non-overlapping across adapters).
128
+ */
129
+ declare const defaultAdapters: readonly Adapter[];
130
+ //#endregion
131
+ export { appkit as a, Container as c, Probe as d, QuitOptions as f, SupportedPlatform as h, chromium as i, DestroyOptions as l, StageResult as m, firefox as n, Adapter as o, QuitResult as p, crashReporter as r, ContainOptions as s, defaultAdapters as t, DestroyResult as u };
132
+ //# sourceMappingURL=index-B1y4yb1N.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-B1y4yb1N.d.mts","names":[],"sources":["../src/types.ts","../src/adapters/appkit.ts","../src/adapters/chromium.ts","../src/adapters/crash-reporter.ts","../src/adapters/firefox.ts","../src/adapters/index.ts"],"mappings":";;;KAWY,iBAAA;AAAA,KAEA,WAAA,GAAc,aAAA;AAAA,KACd,cAAA,GAAiB,gBAAA;AAAA,KACjB,UAAA,GAAa,YAAA;AAAA,KACb,aAAA,GAAgB,eAAA;AAAA,KAChB,WAAA,GAAc,aAAA;AAAA,KACd,KAAA,GAAQ,OAAA;AALpB;;;;AAAA,KAWY,SAAA,GAAY,aAAA;AAVxB;;;;;AACA;;;;;AACA;AAFA,KAuBY,OAAA;EAAA,SACD,IAAA;EACT,OAAA,CAAQ,KAAA,EAAO,KAAA,aAAkB,OAAA;EACjC,UAAA,EAAY,KAAA,EAAO,KAAA,UAAe,OAAA;EAClC,WAAA,EAAa,KAAA,EAAO,KAAA,EAAO,SAAA,kBAA2B,OAAA;EACtD,UAAA,EAAY,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,WAAA,UAAqB,OAAA;EACvD,SAAA,EAAW,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,UAAA,UAAoB,OAAA;EACrD,eAAA,EAAiB,KAAA,EAAO,KAAA,UAAe,OAAA;AAAA;AA1BzC;;;;;AAMA;AANA,KAmCY,cAAA,GAAiB,IAAA,CAAK,gBAAA;EAChC,QAAA,GAAW,OAAA;AAAA;;;;;AA3Cb;;;;;AAEA;;;;;AACA;;;;;cCSa,MAAA,EAAQ,OAAA;;;;;ADZrB;;;;;AAEA;;;;;AACA;;cEea,QAAA,EAAU,OAAA;;;;;AFlBvB;;;;;AAEA;;;;;AACA;;;;;AACA;;;;;AACA;;cGca,aAAA,EAAe,OAAA;;;;;AHnB5B;;;;;AAEA;;;cIGa,OAAA,EAAS,OAAA;;;;;;AJHtB;;cKGa,eAAA,WAA0B,OAAA"}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,29 @@
1
+ import { a as appkit, c as Container, d as Probe, f as QuitOptions, h as SupportedPlatform, i as chromium, l as DestroyOptions, m as StageResult, n as firefox, o as Adapter, p as QuitResult, r as crashReporter, s as ContainOptions, t as defaultAdapters, u as DestroyResult } from "./index-B1y4yb1N.mjs";
1
2
  import { coreVersion } from "@uncontainerizable/native";
2
3
 
3
- //#region src/types.d.ts
4
- type SupportedPlatform = "linux" | "darwin" | "win32";
4
+ //#region src/index.d.ts
5
+ /**
6
+ * Namespaced handle for spawning contained processes.
7
+ *
8
+ * Construct once per application, typically with a reverse-DNS string like
9
+ * `"com.example.my-supervisor"`. The prefix namespaces identity strings so
10
+ * two unrelated libraries using `uncontainerizable` cannot collide.
11
+ *
12
+ * Throws `INVALID_IDENTITY` if the prefix contains characters outside
13
+ * `[A-Za-z0-9._:-]`.
14
+ */
15
+ declare class App {
16
+ #private;
17
+ constructor(prefix: string);
18
+ get prefix(): string;
19
+ /**
20
+ * Spawn a contained process. If `options.identity` is set, any previous
21
+ * instance with the same (prefix, identity) pair is killed before this
22
+ * one launches. If `options.adapters` is non-empty the Rust
23
+ * orchestrator drives their lifecycle hooks around the quit ladder.
24
+ */
25
+ contain(command: string, options?: ContainOptions): Promise<Container>;
26
+ }
5
27
  //#endregion
6
- export { type SupportedPlatform, coreVersion };
28
+ export { type Adapter, App, type ContainOptions, type Container, type DestroyOptions, type DestroyResult, type Probe, type QuitOptions, type QuitResult, type StageResult, type SupportedPlatform, appkit, chromium, coreVersion, crashReporter, defaultAdapters, firefox };
7
29
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;KAAY,iBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;AAwBA;;;;;;;;;cAAa,GAAA;EAAA;cAGC,MAAA;EAAA,IAIR,MAAA,CAAA;EAUI;;;;;;EAAR,OAAA,CAAQ,OAAA,UAAiB,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,SAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -1,3 +1,66 @@
1
- import { coreVersion } from "@uncontainerizable/native";
1
+ import { a as appkit, i as chromium, n as firefox, r as crashReporter, t as defaultAdapters } from "./adapters-BYLFpYWC.mjs";
2
+ import { NodeApp, coreVersion } from "@uncontainerizable/native";
3
+ //#region src/index.ts
4
+ /**
5
+ * Namespaced handle for spawning contained processes.
6
+ *
7
+ * Construct once per application, typically with a reverse-DNS string like
8
+ * `"com.example.my-supervisor"`. The prefix namespaces identity strings so
9
+ * two unrelated libraries using `uncontainerizable` cannot collide.
10
+ *
11
+ * Throws `INVALID_IDENTITY` if the prefix contains characters outside
12
+ * `[A-Za-z0-9._:-]`.
13
+ */
14
+ var App = class {
15
+ #inner;
16
+ constructor(prefix) {
17
+ this.#inner = new NodeApp(prefix);
18
+ }
19
+ get prefix() {
20
+ return this.#inner.prefix;
21
+ }
22
+ /**
23
+ * Spawn a contained process. If `options.identity` is set, any previous
24
+ * instance with the same (prefix, identity) pair is killed before this
25
+ * one launches. If `options.adapters` is non-empty the Rust
26
+ * orchestrator drives their lifecycle hooks around the quit ladder.
27
+ */
28
+ contain(command, options = {}) {
29
+ const { adapters, ...nativeOpts } = options;
30
+ return this.#inner.contain(command, {
31
+ ...nativeOpts,
32
+ adapters: adapters?.map(normalizeAdapter)
33
+ });
34
+ }
35
+ };
36
+ /**
37
+ * Normalize a user-provided `Adapter` (which may declare sync methods)
38
+ * into the always-async shape the napi bridge expects. Every optional
39
+ * hook is only forwarded if defined, so the Rust side keeps its
40
+ * "undefined means skip" semantics.
41
+ */
42
+ function normalizeAdapter(adapter) {
43
+ return {
44
+ name: adapter.name,
45
+ matches: async (probe) => Boolean(await adapter.matches(probe)),
46
+ beforeQuit: adapter.beforeQuit ? async (probe) => {
47
+ await adapter.beforeQuit?.(probe);
48
+ } : void 0,
49
+ beforeStage: adapter.beforeStage ? async (probe, stageName) => {
50
+ await adapter.beforeStage?.(probe, stageName);
51
+ } : void 0,
52
+ afterStage: adapter.afterStage ? async (probe, result) => {
53
+ await adapter.afterStage?.(probe, result);
54
+ } : void 0,
55
+ afterQuit: adapter.afterQuit ? async (probe, result) => {
56
+ await adapter.afterQuit?.(probe, result);
57
+ } : void 0,
58
+ clearCrashState: adapter.clearCrashState ? async (probe) => {
59
+ await adapter.clearCrashState?.(probe);
60
+ } : void 0
61
+ };
62
+ }
63
+ //#endregion
64
+ export { App, appkit, chromium, coreVersion, crashReporter, defaultAdapters, firefox };
2
65
 
3
- export { coreVersion };
66
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["#inner"],"sources":["../src/index.ts"],"sourcesContent":["import { type JsAdapter, NodeApp } from \"@uncontainerizable/native\";\n\nimport type { Adapter, ContainOptions, Container } from \"#/types.js\";\n\nexport { coreVersion } from \"@uncontainerizable/native\";\n\nexport {\n appkit,\n chromium,\n crashReporter,\n defaultAdapters,\n firefox,\n} from \"#/adapters/index.js\";\n\n/**\n * Namespaced handle for spawning contained processes.\n *\n * Construct once per application, typically with a reverse-DNS string like\n * `\"com.example.my-supervisor\"`. The prefix namespaces identity strings so\n * two unrelated libraries using `uncontainerizable` cannot collide.\n *\n * Throws `INVALID_IDENTITY` if the prefix contains characters outside\n * `[A-Za-z0-9._:-]`.\n */\nexport class App {\n readonly #inner: NodeApp;\n\n constructor(prefix: string) {\n this.#inner = new NodeApp(prefix);\n }\n\n get prefix(): string {\n return this.#inner.prefix;\n }\n\n /**\n * Spawn a contained process. If `options.identity` is set, any previous\n * instance with the same (prefix, identity) pair is killed before this\n * one launches. If `options.adapters` is non-empty the Rust\n * orchestrator drives their lifecycle hooks around the quit ladder.\n */\n contain(command: string, options: ContainOptions = {}): Promise<Container> {\n const { adapters, ...nativeOpts } = options;\n return this.#inner.contain(command, {\n ...nativeOpts,\n adapters: adapters?.map(normalizeAdapter),\n });\n }\n}\n\n/**\n * Normalize a user-provided `Adapter` (which may declare sync methods)\n * into the always-async shape the napi bridge expects. Every optional\n * hook is only forwarded if defined, so the Rust side keeps its\n * \"undefined means skip\" semantics.\n */\nfunction normalizeAdapter(adapter: Adapter): JsAdapter {\n return {\n name: adapter.name,\n matches: async (probe) => Boolean(await adapter.matches(probe)),\n beforeQuit: adapter.beforeQuit\n ? async (probe) => {\n await adapter.beforeQuit?.(probe);\n }\n : undefined,\n beforeStage: adapter.beforeStage\n ? async (probe, stageName) => {\n await adapter.beforeStage?.(probe, stageName);\n }\n : undefined,\n afterStage: adapter.afterStage\n ? async (probe, result) => {\n await adapter.afterStage?.(probe, result);\n }\n : undefined,\n afterQuit: adapter.afterQuit\n ? async (probe, result) => {\n await adapter.afterQuit?.(probe, result);\n }\n : undefined,\n clearCrashState: adapter.clearCrashState\n ? async (probe) => {\n await adapter.clearCrashState?.(probe);\n }\n : undefined,\n };\n}\n\nexport type {\n Adapter,\n ContainOptions,\n Container,\n DestroyOptions,\n DestroyResult,\n Probe,\n QuitOptions,\n QuitResult,\n StageResult,\n SupportedPlatform,\n} from \"#/types.js\";\n"],"mappings":";;;;;;;;;;;;;AAwBA,IAAa,MAAb,MAAiB;CACf;CAEA,YAAY,QAAgB;AAC1B,QAAA,QAAc,IAAI,QAAQ,OAAO;;CAGnC,IAAI,SAAiB;AACnB,SAAO,MAAA,MAAY;;;;;;;;CASrB,QAAQ,SAAiB,UAA0B,EAAE,EAAsB;EACzE,MAAM,EAAE,UAAU,GAAG,eAAe;AACpC,SAAO,MAAA,MAAY,QAAQ,SAAS;GAClC,GAAG;GACH,UAAU,UAAU,IAAI,iBAAiB;GAC1C,CAAC;;;;;;;;;AAUN,SAAS,iBAAiB,SAA6B;AACrD,QAAO;EACL,MAAM,QAAQ;EACd,SAAS,OAAO,UAAU,QAAQ,MAAM,QAAQ,QAAQ,MAAM,CAAC;EAC/D,YAAY,QAAQ,aAChB,OAAO,UAAU;AACf,SAAM,QAAQ,aAAa,MAAM;MAEnC,KAAA;EACJ,aAAa,QAAQ,cACjB,OAAO,OAAO,cAAc;AAC1B,SAAM,QAAQ,cAAc,OAAO,UAAU;MAE/C,KAAA;EACJ,YAAY,QAAQ,aAChB,OAAO,OAAO,WAAW;AACvB,SAAM,QAAQ,aAAa,OAAO,OAAO;MAE3C,KAAA;EACJ,WAAW,QAAQ,YACf,OAAO,OAAO,WAAW;AACvB,SAAM,QAAQ,YAAY,OAAO,OAAO;MAE1C,KAAA;EACJ,iBAAiB,QAAQ,kBACrB,OAAO,UAAU;AACf,SAAM,QAAQ,kBAAkB,MAAM;MAExC,KAAA;EACL"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uncontainerizable",
3
- "version": "0.0.3",
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,12 +41,12 @@
41
41
  "provenance": true
42
42
  },
43
43
  "dependencies": {
44
- "@uncontainerizable/native": "0.0.3"
44
+ "@uncontainerizable/native": "0.1.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^24",
48
48
  "publint": "^0.3",
49
- "tsdown": "^0.20",
49
+ "tsdown": "^0.21.7",
50
50
  "typescript": "^6",
51
51
  "vitest": "^4",
52
52
  "@uncontainerizable/tsconfig": "0.0.0"