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 +209 -0
- package/dist/adapters/index.d.mts +2 -1
- package/dist/adapters/index.mjs +2 -1
- package/dist/adapters-BYLFpYWC.mjs +157 -0
- package/dist/adapters-BYLFpYWC.mjs.map +1 -0
- package/dist/index-B1y4yb1N.d.mts +132 -0
- package/dist/index-B1y4yb1N.d.mts.map +1 -0
- package/dist/index.d.mts +25 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +65 -2
- package/dist/index.mjs.map +1 -0
- package/package.json +3 -3
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
|
-
|
|
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 };
|
package/dist/adapters/index.mjs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
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/
|
|
4
|
-
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
44
|
+
"@uncontainerizable/native": "0.1.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^24",
|
|
48
48
|
"publint": "^0.3",
|
|
49
|
-
"tsdown": "^0.
|
|
49
|
+
"tsdown": "^0.21.7",
|
|
50
50
|
"typescript": "^6",
|
|
51
51
|
"vitest": "^4",
|
|
52
52
|
"@uncontainerizable/tsconfig": "0.0.0"
|