@smthrs/platform-bun 1.0.0-rc.8 → 1.0.0-rc.9

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 CHANGED
@@ -23,7 +23,7 @@ package is that object for a process running on Bun.
23
23
  ## Install
24
24
 
25
25
  ```bash
26
- bun add @smthrs/platform-bun@1.0.0-rc.8 effect
26
+ bun add @smthrs/platform-bun@1.0.0-rc.9 effect
27
27
  ```
28
28
 
29
29
  `@smthrs/platform-node` and `effect` are required peers, as `package.json`
@@ -58,12 +58,13 @@ The program names Bun nowhere. It asks for a service tag, and the layer decides
58
58
  what fills it, so swapping `BunHost.layer` for `NodeHost.layer` from
59
59
  `@smthrs/platform-node` does not change a character of it.
60
60
 
61
- The complete host bundles require jj 0.39.0 or newer. Each bundle builds its jj
62
- layer with one version probe; construction can fail with `JjError`, including
63
- `not_installed` or `unsupported_version`. The version probe and repository commands use the
64
- selected process runner. Contained bundles record and retire the probe in the
65
- host process ledger. Probe results are cached per resolved absolute executable
66
- path and runner, with a separate cache for each spawner instance.
61
+ Every host bundle builds without jj. The `Jj` slot needs jj 0.39.0 or newer: its
62
+ first operation runs one version probe and fails with `JjError` `not_installed`
63
+ or `unsupported_version` when jj is missing or older. The version probe and
64
+ repository commands use the selected process runner. Contained bundles record
65
+ and retire the probe in the host process ledger. Probe results are cached per
66
+ resolved absolute executable path and runner, with a separate cache for each
67
+ spawner instance.
67
68
 
68
69
  ## Modules
69
70
 
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import * as BunChildProcessSpawner from "../vendor/cjs/bun/BunChildProcessSpawner.js";
25
25
  import * as BunCrypto from "../vendor/cjs/bun/BunCrypto.js";
26
- import type { Jj, JjError } from "@smthrs/jj";
26
+ import type { Jj } from "@smthrs/jj";
27
27
  import type { HostServiceIds } from "@smthrs/kernel/HostServices";
28
28
  import type * as KernelProcessConfinement from "@smthrs/kernel/ProcessConfinement";
29
29
  import type * as ProcessLedger from "@smthrs/kernel/ProcessLedger";
@@ -69,7 +69,8 @@ export type BunHost = FileSystem | Path.Path | ChildProcessSpawner | Jj | HttpCl
69
69
  *
70
70
  * A code is the contract a caller branches on; the message beside it is for a
71
71
  * person. `invalid_repository_root` is the root handed to {@link layerAt} or
72
- * {@link layerContainedAt} not being absolute, the empty string included.
72
+ * {@link layerContainedAt} not being absolute or containing NUL, the empty
73
+ * string included.
73
74
  *
74
75
  * @category models
75
76
  * @since 1.0.0-rc.0
@@ -147,30 +148,30 @@ export declare const layerHttpClient: Layer.Layer<HttpClient>;
147
148
  * Provides all five Bun Host services, including the runtime-independent Path
148
149
  * service.
149
150
  *
150
- * Fails at construction with {@link JjError} carrying `not_installed` or
151
- * `unsupported_version`: the `Jj` slot is merged eagerly and probes
152
- * `jj --version` while the layer is built, before the program body runs and
153
- * whether or not the program asks for `Jj`. Requires jj 0.39.0 or later.
151
+ * Builds on a host without jj. The `Jj` slot probes `jj --version` on its
152
+ * first operation and fails that operation with `JjError` carrying
153
+ * `not_installed` or `unsupported_version`; a program that never calls `Jj`
154
+ * runs without jj. `Jj` requires jj 0.39.0 or later.
154
155
  *
155
156
  * @category layers
156
157
  * @since 1.0.0-rc.0
157
158
  * @slop
158
159
  */
159
- export declare const layer: Layer.Layer<BunHost | Crypto.Crypto, JjError>;
160
+ export declare const layer: Layer.Layer<BunHost | Crypto.Crypto>;
160
161
  /**
161
162
  * Provides all five Bun Host services bound to one absolute repository root.
162
163
  *
163
164
  * Throws {@link BunHostError} with code `invalid_repository_root` when `root`
164
- * is not absolute; the empty string counts. The check runs when the factory is
165
- * called, before any layer exists.
165
+ * is not absolute or contains NUL; the empty string counts. The check runs
166
+ * when the factory is called, before any layer exists.
166
167
  *
167
- * Fails at construction with {@link JjError} exactly as {@link layer} does:
168
- * the `Jj` slot probes `jj --version` while the layer is built.
168
+ * Builds without jj exactly as {@link layer} does: the `Jj` slot probes
169
+ * `jj --version` on its first operation.
169
170
  *
170
171
  * @category layers
171
172
  * @since 1.0.0-rc.0
172
173
  */
173
- export declare const layerAt: (root: string) => Layer.Layer<BunHost | Crypto.Crypto, JjError>;
174
+ export declare const layerAt: (root: string) => Layer.Layer<BunHost | Crypto.Crypto>;
174
175
  /**
175
176
  * Provides the Bun host with process containment turned on.
176
177
  *
@@ -182,24 +183,23 @@ export declare const layerAt: (root: string) => Layer.Layer<BunHost | Crypto.Cry
182
183
  * it makes, `process.kill` and `taskkill`, are Node's, and Bun implements
183
184
  * them unchanged.
184
185
  *
185
- * Fails at construction with {@link JjError} exactly as {@link layer} does;
186
- * here the `jj --version` probe runs through the contained spawner, so its
186
+ * Builds without jj exactly as {@link layer} does; here the first
187
+ * operation's `jj --version` probe runs through the contained spawner, so its
187
188
  * record enters and leaves the ledger like any other child.
188
189
  *
189
190
  * @category layers
190
191
  * @since 1.0.0-rc.0
191
192
  */
192
- export declare const layerContained: (options?: ContainedOptions) => Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger>;
193
+ export declare const layerContained: (options?: ContainedOptions) => Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger>;
193
194
  /**
194
195
  * Provides the contained Bun host bound to one absolute repository root.
195
196
  *
196
197
  * Refuses a root exactly as {@link layerAt} does: {@link BunHostError} with
197
198
  * code `invalid_repository_root`, thrown when the factory is called.
198
- * Fails at construction with {@link JjError} exactly as {@link layerContained}
199
- * does.
199
+ * Builds without jj exactly as {@link layerContained} does.
200
200
  *
201
201
  * @category layers
202
202
  * @since 1.0.0-rc.0
203
203
  */
204
- export declare const layerContainedAt: (root: string, options?: ContainedOptions) => Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger>;
204
+ export declare const layerContainedAt: (root: string, options?: ContainedOptions) => Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger>;
205
205
  //# sourceMappingURL=BunHost.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BunHost.d.ts","sourceRoot":"","sources":["../../src/BunHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,sBAAsB,MAAM,6CAA6C,CAAA;AACrF,OAAO,KAAK,SAAS,MAAM,gCAAgC,CAAA;AAE3D,OAAO,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,KAAK,KAAK,wBAAwB,MAAM,mCAAmC,CAAA;AAClF,OAAO,KAAK,KAAK,aAAa,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,gBAAgB,MAAM,wCAAwC,CAAA;AAE1E,OAAO,KAAK,YAAY,MAAM,oCAAoC,CAAA;AAElE,OAAO,KAAK,aAAa,MAAM,qCAAqC,CAAA;AACpE,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,KAAK,IAAI,MAAM,aAAa,CAAA;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAA;AAC7E,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA;AAE1G;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,GACf,UAAU,GACV,IAAI,CAAC,IAAI,GACT,mBAAmB,GACnB,EAAE,GACF,UAAU,GACV,wBAAwB,CAAC,kBAAkB,CAAA;AAE/C;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,yBAAyB,CAAA;;;;;AAExD;;;;;;;;;;;GAWG;AACH,qBAAa,YAAa,SAAQ,iBAGhC;IACA,SAAkB,IAAI,kBAAiB;CACxC;AAqDD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAMvF,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC,cAAc,GAAG,aAAa,CAAC,OAAO,CAAA;AAWnF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,CAAuC,CAAA;AAE3F;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAM/D,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CASlF,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,gBAAgB,KACzB,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,aAAa,CAc3E,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,MAAM,EACZ,UAAU,gBAAgB,KACzB,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,aAAa,CAY3E,CAAA"}
1
+ {"version":3,"file":"BunHost.d.ts","sourceRoot":"","sources":["../../src/BunHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,sBAAsB,MAAM,6CAA6C,CAAA;AACrF,OAAO,KAAK,SAAS,MAAM,gCAAgC,CAAA;AAE3D,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,YAAY,CAAA;AAEpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,KAAK,KAAK,wBAAwB,MAAM,mCAAmC,CAAA;AAClF,OAAO,KAAK,KAAK,aAAa,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,gBAAgB,MAAM,wCAAwC,CAAA;AAE1E,OAAO,KAAK,YAAY,MAAM,oCAAoC,CAAA;AAElE,OAAO,KAAK,aAAa,MAAM,qCAAqC,CAAA;AACpE,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,KAAK,IAAI,MAAM,aAAa,CAAA;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAA;AAC7E,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA;AAE1G;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,GACf,UAAU,GACV,IAAI,CAAC,IAAI,GACT,mBAAmB,GACnB,EAAE,GACF,UAAU,GACV,wBAAwB,CAAC,kBAAkB,CAAA;AAE/C;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GAAG,yBAAyB,CAAA;;;;;AAExD;;;;;;;;;;;GAWG;AACH,qBAAa,YAAa,SAAQ,iBAGhC;IACA,SAAkB,IAAI,kBAAiB;CACxC;AA4DD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAMvF,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC,cAAc,GAAG,aAAa,CAAC,OAAO,CAAA;AAWnF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,CAAuC,CAAA;AAE3F;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAMtD,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CASzE,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,gBAAgB,KACzB,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,aAAa,CAczE,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,MAAM,EACZ,UAAU,gBAAgB,KACzB,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,aAAa,CAYzE,CAAA"}
@@ -79,8 +79,9 @@ const preview = (root, budget) => {
79
79
  return `"${excerpt}"${suffix}`;
80
80
  };
81
81
  const absoluteRoot = (factory, root) => {
82
- if ((0, import_node_path.isAbsolute)(root)) return root;
83
- const prefix = `BunHost.${factory} requires an absolute repository root, got `;
82
+ const requirement = !(0, import_node_path.isAbsolute)(root) ? "an absolute repository root" : root.includes("\0") ? "a repository root without NUL" : void 0;
83
+ if (requirement === void 0) return root;
84
+ const prefix = `BunHost.${factory} requires ${requirement}, got `;
84
85
  throw new BunHostError({
85
86
  code: "invalid_repository_root",
86
87
  message: `${prefix}${preview(root, messageLimit - prefix.length)}`
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/BunHost.ts"],
4
- "sourcesContent": ["/**\n * Aggregate Bun Host bundle.\n *\n * Runtime-specific dependencies stay inside this package; callers get the same\n * closed five-service Host surface every other bundle provides, plus native\n * `ProcessConfinement` for guarded commands.\n *\n * There is no Bun shell module: running a command is Effect's\n * `ChildProcessSpawner`, and `@effect/platform-bun`'s implementation is\n * `@effect/platform-node-shared`'s re-exported: the same code on both\n * runtimes, so no runtime detection is needed here.\n *\n * Outgoing HTTP uses the shared EgressHttpClient on both runtimes. Web\n * requests pin approved addresses; ordinary requests retain the shared pool.\n * Redirects remain visible to the capability kernel, and proxy routing follows\n * the host environment.\n *\n * The filesystem slot is `@smthrs/platform-node`'s `AtomicFileSystem`, byte for\n * byte the layer `NodeHost` uses, so a guarded path operation is\n * descriptor-relative and no-follow on both runtimes.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as BunChildProcessSpawner from \"@effect/platform-bun/BunChildProcessSpawner\"\nimport * as BunCrypto from \"@effect/platform-bun/BunCrypto\"\nimport * as BunPath from \"@effect/platform-bun/BunPath\"\nimport type { Jj, JjError } from \"@smthrs/jj\"\nimport * as BunJj from \"@smthrs/jj/bun/BunJj\"\nimport type { HostServiceIds } from \"@smthrs/kernel/HostServices\"\nimport type * as KernelProcessConfinement from \"@smthrs/kernel/ProcessConfinement\"\nimport type * as ProcessLedger from \"@smthrs/kernel/ProcessLedger\"\nimport * as AtomicFileSystem from \"@smthrs/platform-node/AtomicFileSystem\"\nimport * as EgressHttpClient from \"@smthrs/platform-node/EgressHttpClient\"\nimport * as HostLiveness from \"@smthrs/platform-node/HostLiveness\"\nimport * as ProcessConfinement from \"@smthrs/platform-node/ProcessConfinement\"\nimport * as ProcessReaper from \"@smthrs/platform-node/ProcessReaper\"\nimport type * as Crypto from \"effect/Crypto\"\nimport type { FileSystem } from \"effect/FileSystem\"\nimport type { HttpClient } from \"effect/http/HttpClient\"\nimport * as Layer from \"effect/Layer\"\nimport type * as Path from \"effect/Path\"\nimport type { ChildProcessSpawner } from \"effect/process/ChildProcessSpawner\"\nimport * as Schema from \"effect/Schema\"\nimport { isAbsolute } from \"node:path\"\nimport * as BunFileSystem from \"./BunFileSystem.ts\"\n\n/**\n * Bun platform modules for selectively providing individual services.\n *\n * `AtomicFileSystem` is here for the same reason `NodeHost` re-exports it: it\n * owns the only configuration escape hatch the filesystem slot has, and a Bun\n * program with a different packaged helper path needs to reach\n * `AtomicFileSystem.layerWith` without hand-composing the other four tags.\n *\n * `BunJj` is deliberately absent: it belongs to `@smthrs/jj` and is imported\n * from there, never re-exported here. So is `@effect/platform-bun/BunHttpClient`:\n * its `layer` follows redirects to origins the capability kernel never\n * authorized. The network slot on its own is {@link layerHttpClient}.\n *\n * @category re-exports\n * @since 1.0.0-rc.0\n * @slop\n */\nexport { AtomicFileSystem, BunChildProcessSpawner, BunCrypto, BunFileSystem, HostLiveness, ProcessReaper }\n\n/**\n * The complete closed Host service union provided by Bun.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport type BunHost =\n | FileSystem\n | Path.Path\n | ChildProcessSpawner\n | Jj\n | HttpClient\n | KernelProcessConfinement.ProcessConfinement\n\n/**\n * The stable codes a `BunHost` factory refuses with.\n *\n * A code is the contract a caller branches on; the message beside it is for a\n * person. `invalid_repository_root` is the root handed to {@link layerAt} or\n * {@link layerContainedAt} not being absolute, the empty string included.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type BunHostErrorCode = \"invalid_repository_root\"\n\n/**\n * The refusal a `BunHost` factory throws before it builds a layer.\n *\n * It is thrown rather than failed because a factory is called while a program\n * composes its layers, where there is no fiber to fail yet and a wrong root is\n * a composition mistake, not a runtime outcome. It is this package's class,\n * not the `Jj` adapter's, because a Bun caller composes `BunHost` and should\n * learn nothing about the adapter behind the `Jj` slot from an error message.\n *\n * @category errors\n * @since 1.0.0-rc.0\n */\nexport class BunHostError extends Schema.TaggedError<BunHostError>()(\"@smthrs/platform-bun/BunHostError\", {\n code: Schema.Literal(\"invalid_repository_root\"),\n message: Schema.String\n}) {\n override readonly name = \"BunHostError\"\n}\n\n/** The longest message a factory refuses with, in UTF-16 code units. */\nconst messageLimit = 255\n\n/** How many code points of a refused root the message repeats at most. */\nconst previewLimit = 64\n\n/**\n * A bounded, quoted excerpt of a refused root that fits in `budget` code\n * units of the message.\n *\n * Counted in code points rather than UTF-16 units so a cut never splits a\n * surrogate pair, and passed through `JSON.stringify` so a newline in the root\n * cannot break the log line the message lands on. Each code point is encoded\n * on its own and admitted only while the quoted excerpt plus the length\n * suffix still fit, so a control character that widens to a `\\u0001` escape\n * shortens the excerpt instead of lengthening the message, and a cut never\n * lands inside an escape.\n */\nconst preview = (root: string, budget: number): string => {\n const points = Array.from(root)\n const whole = JSON.stringify(root)\n if (points.length <= previewLimit && whole.length <= budget) return whole\n const suffix = `... (${points.length} characters)`\n const room = budget - suffix.length - 2\n let excerpt = \"\"\n for (const point of points.slice(0, previewLimit)) {\n const encoded = JSON.stringify(point).slice(1, -1)\n if (excerpt.length + encoded.length > room) break\n excerpt += encoded\n }\n return `\"${excerpt}\"${suffix}`\n}\n\n/**\n * Refuses a root the `Jj` adapter underneath would refuse, here and in this\n * package's name.\n *\n * `BunJj.layerAt` and `BunJj.layerSpawnerAt` are `NodeJj`'s, and those throw a\n * bare `TypeError` that names `NodeJj`, carries no code, and echoes the whole\n * string. The predicate here is the same `isAbsolute` the adapter applies, so\n * a root that passes this check cannot reach that throw.\n */\nconst absoluteRoot = (factory: \"layerAt\" | \"layerContainedAt\", root: string): string => {\n if (isAbsolute(root)) return root\n const prefix = `BunHost.${factory} requires an absolute repository root, got `\n throw new BunHostError({\n code: \"invalid_repository_root\",\n message: `${prefix}${preview(root, messageLimit - prefix.length)}`\n })\n}\n\n/**\n * Stable implementation identities keyed by the closed Host service slots.\n *\n * Each value names the module behind that slot in the raw host bundle.\n * Contained POSIX factories replace the spawner with {@link ProcessReaper.layerSpawner}.\n * These are identity\n * tokens rather than import specifiers: the filesystem entry is\n * `@smthrs/platform-node/AtomicFileSystem` because that is the implementation,\n * even though a consumer reaches it through `@smthrs/platform-bun`.\n *\n * Nothing digests them yet. `@smthrs/plan`'s step key carries a `layers`\n * component these values are meant to feed, but no planner derives it from a\n * host bundle today, so changing one invalidates no cached step.\n *\n * The keys are written as literals rather than `HostServiceIds` positions so\n * that reordering the closed list cannot silently pair a slot with another\n * slot's implementation.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const implementationIds: Readonly<Record<(typeof HostServiceIds)[number], string>> = {\n \"effect/FileSystem\": \"@smthrs/platform-node/AtomicFileSystem\",\n \"effect/Path\": \"@effect/platform-bun/BunPath\",\n \"effect/process/ChildProcessSpawner\": \"@effect/platform-bun/BunChildProcessSpawner\",\n \"@smthrs/jj/Jj\": \"@smthrs/jj/bun/BunJj\",\n \"effect/HttpClient\": \"@smthrs/platform-node/EgressHttpClient\"\n}\n\n/**\n * What a caller may configure about containment.\n *\n * `ContainedSpawner.Options.platform` is deliberately not part of it. It\n * decides one thing, whether a command that names no `detached` option gets a\n * process group of its own, and this native host detaches by `process.platform`\n * whatever a record claims. A\n * caller-supplied `\"win32\"` on a POSIX host would therefore record\n * `pgid: null` for a child that really does lead a group, and\n * {@link ProcessReaper.reap} would retire that record as `no-group` and leave\n * the orphan running forever: a durable lie rather than a compile error.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type ContainedOptions = ProcessReaper.SpawnerOptions & ProcessReaper.Options\n\n/** The reaper half of {@link ContainedOptions}. */\nconst reaping = (options?: ContainedOptions): ProcessReaper.Options => ({\n ownerPid: options?.ownerPid,\n system: options?.system\n})\n\n/** The two services `BunChildProcessSpawner` resolves paths and files with. */\nconst platform = Layer.mergeAll(BunFileSystem.layer, BunPath.layer, BunCrypto.layer)\n\n/**\n * The network slot on its own: the shared egress client, which never follows a\n * redirect. A `3xx` comes back to the caller with its `location` intact and the\n * second origin uncontacted; following a redirect is `@smthrs/kernel`'s guarded\n * `HttpClient.layer`, which rechecks every hop.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerHttpClient: Layer.Layer<HttpClient> = EgressHttpClient.layer(process.env)\n\n/**\n * Provides all five Bun Host services, including the runtime-independent Path\n * service.\n *\n * Fails at construction with {@link JjError} carrying `not_installed` or\n * `unsupported_version`: the `Jj` slot is merged eagerly and probes\n * `jj --version` while the layer is built, before the program body runs and\n * whether or not the program asks for `Jj`. Requires jj 0.39.0 or later.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layer: Layer.Layer<BunHost | Crypto.Crypto, JjError> = Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n Layer.provide(BunChildProcessSpawner.layer, platform),\n BunJj.layer,\n layerHttpClient\n)\n\n/**\n * Provides all five Bun Host services bound to one absolute repository root.\n *\n * Throws {@link BunHostError} with code `invalid_repository_root` when `root`\n * is not absolute; the empty string counts. The check runs when the factory is\n * called, before any layer exists.\n *\n * Fails at construction with {@link JjError} exactly as {@link layer} does:\n * the `Jj` slot probes `jj --version` while the layer is built.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerAt = (root: string): Layer.Layer<BunHost | Crypto.Crypto, JjError> => {\n const repositoryRoot = absoluteRoot(\"layerAt\", root)\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n Layer.provide(BunChildProcessSpawner.layer, platform),\n BunJj.layerAt(repositoryRoot),\n layerHttpClient\n )\n}\n\n/**\n * Provides the Bun host with process containment turned on.\n *\n * Bun uses the same prepared native process lifecycle as Node on POSIX, while\n * preserving its underlying runtime spawner on Windows. `ContainedSpawner` gives\n * every child an escalation deadline and a ledger entry, and\n * `ProcessReaper` sweeps the entries a crashed incarnation of this host left\n * behind. The reaper module lives in `@smthrs/platform-node` because the calls\n * it makes, `process.kill` and `taskkill`, are Node's, and Bun implements\n * them unchanged.\n *\n * Fails at construction with {@link JjError} exactly as {@link layer} does;\n * here the `jj --version` probe runs through the contained spawner, so its\n * record enters and leaves the ledger like any other child.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerContained = (\n options?: ContainedOptions\n): Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger> => {\n const spawner = Layer.provide(\n ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),\n Layer.provide(BunChildProcessSpawner.layer, platform)\n )\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n layerHttpClient,\n // jj goes through the CONTAINED spawner here, not around it, exactly as in\n // `NodeHost.layerContained`: a jj child that starts its own process leads\n // no recorded group, is in no ledger, and outlives the host that ran it.\n Layer.provideMerge(BunJj.layerSpawner, spawner)\n ).pipe(Layer.provideMerge(ProcessReaper.layer(reaping(options))))\n}\n\n/**\n * Provides the contained Bun host bound to one absolute repository root.\n *\n * Refuses a root exactly as {@link layerAt} does: {@link BunHostError} with\n * code `invalid_repository_root`, thrown when the factory is called.\n * Fails at construction with {@link JjError} exactly as {@link layerContained}\n * does.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerContainedAt = (\n root: string,\n options?: ContainedOptions\n): Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger> => {\n const repositoryRoot = absoluteRoot(\"layerContainedAt\", root)\n const spawner = Layer.provide(\n ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),\n Layer.provide(BunChildProcessSpawner.layer, platform)\n )\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n layerHttpClient,\n Layer.provideMerge(BunJj.layerSpawnerAt(repositoryRoot), spawner)\n ).pipe(Layer.provideMerge(ProcessReaper.layer(reaping(options))))\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Aggregate Bun Host bundle.\n *\n * Runtime-specific dependencies stay inside this package; callers get the same\n * closed five-service Host surface every other bundle provides, plus native\n * `ProcessConfinement` for guarded commands.\n *\n * There is no Bun shell module: running a command is Effect's\n * `ChildProcessSpawner`, and `@effect/platform-bun`'s implementation is\n * `@effect/platform-node-shared`'s re-exported: the same code on both\n * runtimes, so no runtime detection is needed here.\n *\n * Outgoing HTTP uses the shared EgressHttpClient on both runtimes. Web\n * requests pin approved addresses; ordinary requests retain the shared pool.\n * Redirects remain visible to the capability kernel, and proxy routing follows\n * the host environment.\n *\n * The filesystem slot is `@smthrs/platform-node`'s `AtomicFileSystem`, byte for\n * byte the layer `NodeHost` uses, so a guarded path operation is\n * descriptor-relative and no-follow on both runtimes.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as BunChildProcessSpawner from \"@effect/platform-bun/BunChildProcessSpawner\"\nimport * as BunCrypto from \"@effect/platform-bun/BunCrypto\"\nimport * as BunPath from \"@effect/platform-bun/BunPath\"\nimport type { Jj } from \"@smthrs/jj\"\nimport * as BunJj from \"@smthrs/jj/bun/BunJj\"\nimport type { HostServiceIds } from \"@smthrs/kernel/HostServices\"\nimport type * as KernelProcessConfinement from \"@smthrs/kernel/ProcessConfinement\"\nimport type * as ProcessLedger from \"@smthrs/kernel/ProcessLedger\"\nimport * as AtomicFileSystem from \"@smthrs/platform-node/AtomicFileSystem\"\nimport * as EgressHttpClient from \"@smthrs/platform-node/EgressHttpClient\"\nimport * as HostLiveness from \"@smthrs/platform-node/HostLiveness\"\nimport * as ProcessConfinement from \"@smthrs/platform-node/ProcessConfinement\"\nimport * as ProcessReaper from \"@smthrs/platform-node/ProcessReaper\"\nimport type * as Crypto from \"effect/Crypto\"\nimport type { FileSystem } from \"effect/FileSystem\"\nimport type { HttpClient } from \"effect/http/HttpClient\"\nimport * as Layer from \"effect/Layer\"\nimport type * as Path from \"effect/Path\"\nimport type { ChildProcessSpawner } from \"effect/process/ChildProcessSpawner\"\nimport * as Schema from \"effect/Schema\"\nimport { isAbsolute } from \"node:path\"\nimport * as BunFileSystem from \"./BunFileSystem.ts\"\n\n/**\n * Bun platform modules for selectively providing individual services.\n *\n * `AtomicFileSystem` is here for the same reason `NodeHost` re-exports it: it\n * owns the only configuration escape hatch the filesystem slot has, and a Bun\n * program with a different packaged helper path needs to reach\n * `AtomicFileSystem.layerWith` without hand-composing the other four tags.\n *\n * `BunJj` is deliberately absent: it belongs to `@smthrs/jj` and is imported\n * from there, never re-exported here. So is `@effect/platform-bun/BunHttpClient`:\n * its `layer` follows redirects to origins the capability kernel never\n * authorized. The network slot on its own is {@link layerHttpClient}.\n *\n * @category re-exports\n * @since 1.0.0-rc.0\n * @slop\n */\nexport { AtomicFileSystem, BunChildProcessSpawner, BunCrypto, BunFileSystem, HostLiveness, ProcessReaper }\n\n/**\n * The complete closed Host service union provided by Bun.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport type BunHost =\n | FileSystem\n | Path.Path\n | ChildProcessSpawner\n | Jj\n | HttpClient\n | KernelProcessConfinement.ProcessConfinement\n\n/**\n * The stable codes a `BunHost` factory refuses with.\n *\n * A code is the contract a caller branches on; the message beside it is for a\n * person. `invalid_repository_root` is the root handed to {@link layerAt} or\n * {@link layerContainedAt} not being absolute or containing NUL, the empty\n * string included.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type BunHostErrorCode = \"invalid_repository_root\"\n\n/**\n * The refusal a `BunHost` factory throws before it builds a layer.\n *\n * It is thrown rather than failed because a factory is called while a program\n * composes its layers, where there is no fiber to fail yet and a wrong root is\n * a composition mistake, not a runtime outcome. It is this package's class,\n * not the `Jj` adapter's, because a Bun caller composes `BunHost` and should\n * learn nothing about the adapter behind the `Jj` slot from an error message.\n *\n * @category errors\n * @since 1.0.0-rc.0\n */\nexport class BunHostError extends Schema.TaggedError<BunHostError>()(\"@smthrs/platform-bun/BunHostError\", {\n code: Schema.Literal(\"invalid_repository_root\"),\n message: Schema.String\n}) {\n override readonly name = \"BunHostError\"\n}\n\n/** The longest message a factory refuses with, in UTF-16 code units. */\nconst messageLimit = 255\n\n/** How many code points of a refused root the message repeats at most. */\nconst previewLimit = 64\n\n/**\n * A bounded, quoted excerpt of a refused root that fits in `budget` code\n * units of the message.\n *\n * Counted in code points rather than UTF-16 units so a cut never splits a\n * surrogate pair, and passed through `JSON.stringify` so a newline in the root\n * cannot break the log line the message lands on. Each code point is encoded\n * on its own and admitted only while the quoted excerpt plus the length\n * suffix still fit, so a control character that widens to a `\\u0001` escape\n * shortens the excerpt instead of lengthening the message, and a cut never\n * lands inside an escape.\n */\nconst preview = (root: string, budget: number): string => {\n const points = Array.from(root)\n const whole = JSON.stringify(root)\n if (points.length <= previewLimit && whole.length <= budget) return whole\n const suffix = `... (${points.length} characters)`\n const room = budget - suffix.length - 2\n let excerpt = \"\"\n for (const point of points.slice(0, previewLimit)) {\n const encoded = JSON.stringify(point).slice(1, -1)\n if (excerpt.length + encoded.length > room) break\n excerpt += encoded\n }\n return `\"${excerpt}\"${suffix}`\n}\n\n/**\n * Refuses a root the `Jj` adapter underneath would refuse, here and in this\n * package's name.\n *\n * `BunJj.layerAt` and `BunJj.layerSpawnerAt` are `NodeJj`'s, and those throw a\n * bare `TypeError` that names `NodeJj`, carries no code, and echoes the whole\n * string. The predicate includes the same `isAbsolute` the adapter applies, so\n * a root that passes this check cannot reach that throw. An absolute root\n * containing NUL is refused too: no filesystem or process API can represent it.\n */\nconst absoluteRoot = (factory: \"layerAt\" | \"layerContainedAt\", root: string): string => {\n // An absolute root with NUL names that defect, not absoluteness it already has.\n const requirement = !isAbsolute(root)\n ? \"an absolute repository root\"\n : root.includes(\"\\0\")\n ? \"a repository root without NUL\"\n : undefined\n if (requirement === undefined) return root\n const prefix = `BunHost.${factory} requires ${requirement}, got `\n throw new BunHostError({\n code: \"invalid_repository_root\",\n message: `${prefix}${preview(root, messageLimit - prefix.length)}`\n })\n}\n\n/**\n * Stable implementation identities keyed by the closed Host service slots.\n *\n * Each value names the module behind that slot in the raw host bundle.\n * Contained POSIX factories replace the spawner with {@link ProcessReaper.layerSpawner}.\n * These are identity\n * tokens rather than import specifiers: the filesystem entry is\n * `@smthrs/platform-node/AtomicFileSystem` because that is the implementation,\n * even though a consumer reaches it through `@smthrs/platform-bun`.\n *\n * Nothing digests them yet. `@smthrs/plan`'s step key carries a `layers`\n * component these values are meant to feed, but no planner derives it from a\n * host bundle today, so changing one invalidates no cached step.\n *\n * The keys are written as literals rather than `HostServiceIds` positions so\n * that reordering the closed list cannot silently pair a slot with another\n * slot's implementation.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const implementationIds: Readonly<Record<(typeof HostServiceIds)[number], string>> = {\n \"effect/FileSystem\": \"@smthrs/platform-node/AtomicFileSystem\",\n \"effect/Path\": \"@effect/platform-bun/BunPath\",\n \"effect/process/ChildProcessSpawner\": \"@effect/platform-bun/BunChildProcessSpawner\",\n \"@smthrs/jj/Jj\": \"@smthrs/jj/bun/BunJj\",\n \"effect/HttpClient\": \"@smthrs/platform-node/EgressHttpClient\"\n}\n\n/**\n * What a caller may configure about containment.\n *\n * `ContainedSpawner.Options.platform` is deliberately not part of it. It\n * decides one thing, whether a command that names no `detached` option gets a\n * process group of its own, and this native host detaches by `process.platform`\n * whatever a record claims. A\n * caller-supplied `\"win32\"` on a POSIX host would therefore record\n * `pgid: null` for a child that really does lead a group, and\n * {@link ProcessReaper.reap} would retire that record as `no-group` and leave\n * the orphan running forever: a durable lie rather than a compile error.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type ContainedOptions = ProcessReaper.SpawnerOptions & ProcessReaper.Options\n\n/** The reaper half of {@link ContainedOptions}. */\nconst reaping = (options?: ContainedOptions): ProcessReaper.Options => ({\n ownerPid: options?.ownerPid,\n system: options?.system\n})\n\n/** The two services `BunChildProcessSpawner` resolves paths and files with. */\nconst platform = Layer.mergeAll(BunFileSystem.layer, BunPath.layer, BunCrypto.layer)\n\n/**\n * The network slot on its own: the shared egress client, which never follows a\n * redirect. A `3xx` comes back to the caller with its `location` intact and the\n * second origin uncontacted; following a redirect is `@smthrs/kernel`'s guarded\n * `HttpClient.layer`, which rechecks every hop.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerHttpClient: Layer.Layer<HttpClient> = EgressHttpClient.layer(process.env)\n\n/**\n * Provides all five Bun Host services, including the runtime-independent Path\n * service.\n *\n * Builds on a host without jj. The `Jj` slot probes `jj --version` on its\n * first operation and fails that operation with `JjError` carrying\n * `not_installed` or `unsupported_version`; a program that never calls `Jj`\n * runs without jj. `Jj` requires jj 0.39.0 or later.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layer: Layer.Layer<BunHost | Crypto.Crypto> = Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n Layer.provide(BunChildProcessSpawner.layer, platform),\n BunJj.layer,\n layerHttpClient\n)\n\n/**\n * Provides all five Bun Host services bound to one absolute repository root.\n *\n * Throws {@link BunHostError} with code `invalid_repository_root` when `root`\n * is not absolute or contains NUL; the empty string counts. The check runs\n * when the factory is called, before any layer exists.\n *\n * Builds without jj exactly as {@link layer} does: the `Jj` slot probes\n * `jj --version` on its first operation.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerAt = (root: string): Layer.Layer<BunHost | Crypto.Crypto> => {\n const repositoryRoot = absoluteRoot(\"layerAt\", root)\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n Layer.provide(BunChildProcessSpawner.layer, platform),\n BunJj.layerAt(repositoryRoot),\n layerHttpClient\n )\n}\n\n/**\n * Provides the Bun host with process containment turned on.\n *\n * Bun uses the same prepared native process lifecycle as Node on POSIX, while\n * preserving its underlying runtime spawner on Windows. `ContainedSpawner` gives\n * every child an escalation deadline and a ledger entry, and\n * `ProcessReaper` sweeps the entries a crashed incarnation of this host left\n * behind. The reaper module lives in `@smthrs/platform-node` because the calls\n * it makes, `process.kill` and `taskkill`, are Node's, and Bun implements\n * them unchanged.\n *\n * Builds without jj exactly as {@link layer} does; here the first\n * operation's `jj --version` probe runs through the contained spawner, so its\n * record enters and leaves the ledger like any other child.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerContained = (\n options?: ContainedOptions\n): Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger> => {\n const spawner = Layer.provide(\n ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),\n Layer.provide(BunChildProcessSpawner.layer, platform)\n )\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n layerHttpClient,\n // jj goes through the CONTAINED spawner here, not around it, exactly as in\n // `NodeHost.layerContained`: a jj child that starts its own process leads\n // no recorded group, is in no ledger, and outlives the host that ran it.\n Layer.provideMerge(BunJj.layerSpawner, spawner)\n ).pipe(Layer.provideMerge(ProcessReaper.layer(reaping(options))))\n}\n\n/**\n * Provides the contained Bun host bound to one absolute repository root.\n *\n * Refuses a root exactly as {@link layerAt} does: {@link BunHostError} with\n * code `invalid_repository_root`, thrown when the factory is called.\n * Builds without jj exactly as {@link layerContained} does.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerContainedAt = (\n root: string,\n options?: ContainedOptions\n): Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger> => {\n const repositoryRoot = absoluteRoot(\"layerContainedAt\", root)\n const spawner = Layer.provide(\n ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),\n Layer.provide(BunChildProcessSpawner.layer, platform)\n )\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n layerHttpClient,\n Layer.provideMerge(BunJj.layerSpawnerAt(repositoryRoot), spawner)\n ).pipe(Layer.provideMerge(ProcessReaper.layer(reaping(options))))\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import * as BunChildProcessSpawner from "../vendor/bun/BunChildProcessSpawner.js";
25
25
  import * as BunCrypto from "../vendor/bun/BunCrypto.js";
26
- import type { Jj, JjError } from "@smthrs/jj";
26
+ import type { Jj } from "@smthrs/jj";
27
27
  import type { HostServiceIds } from "@smthrs/kernel/HostServices";
28
28
  import type * as KernelProcessConfinement from "@smthrs/kernel/ProcessConfinement";
29
29
  import type * as ProcessLedger from "@smthrs/kernel/ProcessLedger";
@@ -69,7 +69,8 @@ export type BunHost = FileSystem | Path.Path | ChildProcessSpawner | Jj | HttpCl
69
69
  *
70
70
  * A code is the contract a caller branches on; the message beside it is for a
71
71
  * person. `invalid_repository_root` is the root handed to {@link layerAt} or
72
- * {@link layerContainedAt} not being absolute, the empty string included.
72
+ * {@link layerContainedAt} not being absolute or containing NUL, the empty
73
+ * string included.
73
74
  *
74
75
  * @category models
75
76
  * @since 1.0.0-rc.0
@@ -147,30 +148,30 @@ export declare const layerHttpClient: Layer.Layer<HttpClient>;
147
148
  * Provides all five Bun Host services, including the runtime-independent Path
148
149
  * service.
149
150
  *
150
- * Fails at construction with {@link JjError} carrying `not_installed` or
151
- * `unsupported_version`: the `Jj` slot is merged eagerly and probes
152
- * `jj --version` while the layer is built, before the program body runs and
153
- * whether or not the program asks for `Jj`. Requires jj 0.39.0 or later.
151
+ * Builds on a host without jj. The `Jj` slot probes `jj --version` on its
152
+ * first operation and fails that operation with `JjError` carrying
153
+ * `not_installed` or `unsupported_version`; a program that never calls `Jj`
154
+ * runs without jj. `Jj` requires jj 0.39.0 or later.
154
155
  *
155
156
  * @category layers
156
157
  * @since 1.0.0-rc.0
157
158
  * @slop
158
159
  */
159
- export declare const layer: Layer.Layer<BunHost | Crypto.Crypto, JjError>;
160
+ export declare const layer: Layer.Layer<BunHost | Crypto.Crypto>;
160
161
  /**
161
162
  * Provides all five Bun Host services bound to one absolute repository root.
162
163
  *
163
164
  * Throws {@link BunHostError} with code `invalid_repository_root` when `root`
164
- * is not absolute; the empty string counts. The check runs when the factory is
165
- * called, before any layer exists.
165
+ * is not absolute or contains NUL; the empty string counts. The check runs
166
+ * when the factory is called, before any layer exists.
166
167
  *
167
- * Fails at construction with {@link JjError} exactly as {@link layer} does:
168
- * the `Jj` slot probes `jj --version` while the layer is built.
168
+ * Builds without jj exactly as {@link layer} does: the `Jj` slot probes
169
+ * `jj --version` on its first operation.
169
170
  *
170
171
  * @category layers
171
172
  * @since 1.0.0-rc.0
172
173
  */
173
- export declare const layerAt: (root: string) => Layer.Layer<BunHost | Crypto.Crypto, JjError>;
174
+ export declare const layerAt: (root: string) => Layer.Layer<BunHost | Crypto.Crypto>;
174
175
  /**
175
176
  * Provides the Bun host with process containment turned on.
176
177
  *
@@ -182,24 +183,23 @@ export declare const layerAt: (root: string) => Layer.Layer<BunHost | Crypto.Cry
182
183
  * it makes, `process.kill` and `taskkill`, are Node's, and Bun implements
183
184
  * them unchanged.
184
185
  *
185
- * Fails at construction with {@link JjError} exactly as {@link layer} does;
186
- * here the `jj --version` probe runs through the contained spawner, so its
186
+ * Builds without jj exactly as {@link layer} does; here the first
187
+ * operation's `jj --version` probe runs through the contained spawner, so its
187
188
  * record enters and leaves the ledger like any other child.
188
189
  *
189
190
  * @category layers
190
191
  * @since 1.0.0-rc.0
191
192
  */
192
- export declare const layerContained: (options?: ContainedOptions) => Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger>;
193
+ export declare const layerContained: (options?: ContainedOptions) => Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger>;
193
194
  /**
194
195
  * Provides the contained Bun host bound to one absolute repository root.
195
196
  *
196
197
  * Refuses a root exactly as {@link layerAt} does: {@link BunHostError} with
197
198
  * code `invalid_repository_root`, thrown when the factory is called.
198
- * Fails at construction with {@link JjError} exactly as {@link layerContained}
199
- * does.
199
+ * Builds without jj exactly as {@link layerContained} does.
200
200
  *
201
201
  * @category layers
202
202
  * @since 1.0.0-rc.0
203
203
  */
204
- export declare const layerContainedAt: (root: string, options?: ContainedOptions) => Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger>;
204
+ export declare const layerContainedAt: (root: string, options?: ContainedOptions) => Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger>;
205
205
  //# sourceMappingURL=BunHost.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BunHost.d.ts","sourceRoot":"","sources":["../../src/BunHost.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BunHost.d.ts","sourceRoot":"","sources":["../../src/BunHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,sBAAsB,MAAM,6CAA6C,CAAA;AACrF,OAAO,KAAK,SAAS,MAAM,gCAAgC,CAAA;AAE3D,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,YAAY,CAAA;AAEpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,KAAK,KAAK,wBAAwB,MAAM,mCAAmC,CAAA;AAClF,OAAO,KAAK,KAAK,aAAa,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,gBAAgB,MAAM,wCAAwC,CAAA;AAE1E,OAAO,KAAK,YAAY,MAAM,oCAAoC,CAAA;AAElE,OAAO,KAAK,aAAa,MAAM,qCAAqC,CAAA;AACpE,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,KAAK,IAAI,MAAM,aAAa,CAAA;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAA;AAC7E,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA;AAE1G;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,GACf,UAAU,GACV,IAAI,CAAC,IAAI,GACT,mBAAmB,GACnB,EAAE,GACF,UAAU,GACV,wBAAwB,CAAC,kBAAkB,CAAA;AAE/C;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GAAG,yBAAyB,CAAA;;;;;AAExD;;;;;;;;;;;GAWG;AACH,qBAAa,YAAa,SAAQ,iBAGhC;IACA,SAAkB,IAAI,kBAAiB;CACxC;AA4DD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAMvF,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC,cAAc,GAAG,aAAa,CAAC,OAAO,CAAA;AAWnF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,CAAuC,CAAA;AAE3F;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAMtD,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CASzE,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,gBAAgB,KACzB,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,aAAa,CAczE,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,MAAM,EACZ,UAAU,gBAAgB,KACzB,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,aAAa,CAYzE,CAAA"}
@@ -108,13 +108,20 @@ const preview = (root, budget) => {
108
108
  *
109
109
  * `BunJj.layerAt` and `BunJj.layerSpawnerAt` are `NodeJj`'s, and those throw a
110
110
  * bare `TypeError` that names `NodeJj`, carries no code, and echoes the whole
111
- * string. The predicate here is the same `isAbsolute` the adapter applies, so
112
- * a root that passes this check cannot reach that throw.
111
+ * string. The predicate includes the same `isAbsolute` the adapter applies, so
112
+ * a root that passes this check cannot reach that throw. An absolute root
113
+ * containing NUL is refused too: no filesystem or process API can represent it.
113
114
  */
114
115
  const absoluteRoot = (factory, root) => {
115
- if (isAbsolute(root))
116
+ // An absolute root with NUL names that defect, not absoluteness it already has.
117
+ const requirement = !isAbsolute(root)
118
+ ? "an absolute repository root"
119
+ : root.includes("\0")
120
+ ? "a repository root without NUL"
121
+ : undefined;
122
+ if (requirement === undefined)
116
123
  return root;
117
- const prefix = `BunHost.${factory} requires an absolute repository root, got `;
124
+ const prefix = `BunHost.${factory} requires ${requirement}, got `;
118
125
  throw new BunHostError({
119
126
  code: "invalid_repository_root",
120
127
  message: `${prefix}${preview(root, messageLimit - prefix.length)}`
@@ -170,10 +177,10 @@ export const layerHttpClient = EgressHttpClient.layer(process.env);
170
177
  * Provides all five Bun Host services, including the runtime-independent Path
171
178
  * service.
172
179
  *
173
- * Fails at construction with {@link JjError} carrying `not_installed` or
174
- * `unsupported_version`: the `Jj` slot is merged eagerly and probes
175
- * `jj --version` while the layer is built, before the program body runs and
176
- * whether or not the program asks for `Jj`. Requires jj 0.39.0 or later.
180
+ * Builds on a host without jj. The `Jj` slot probes `jj --version` on its
181
+ * first operation and fails that operation with `JjError` carrying
182
+ * `not_installed` or `unsupported_version`; a program that never calls `Jj`
183
+ * runs without jj. `Jj` requires jj 0.39.0 or later.
177
184
  *
178
185
  * @category layers
179
186
  * @since 1.0.0-rc.0
@@ -184,11 +191,11 @@ export const layer = Layer.mergeAll(ProcessConfinement.layer(), platform, Layer.
184
191
  * Provides all five Bun Host services bound to one absolute repository root.
185
192
  *
186
193
  * Throws {@link BunHostError} with code `invalid_repository_root` when `root`
187
- * is not absolute; the empty string counts. The check runs when the factory is
188
- * called, before any layer exists.
194
+ * is not absolute or contains NUL; the empty string counts. The check runs
195
+ * when the factory is called, before any layer exists.
189
196
  *
190
- * Fails at construction with {@link JjError} exactly as {@link layer} does:
191
- * the `Jj` slot probes `jj --version` while the layer is built.
197
+ * Builds without jj exactly as {@link layer} does: the `Jj` slot probes
198
+ * `jj --version` on its first operation.
192
199
  *
193
200
  * @category layers
194
201
  * @since 1.0.0-rc.0
@@ -208,8 +215,8 @@ export const layerAt = (root) => {
208
215
  * it makes, `process.kill` and `taskkill`, are Node's, and Bun implements
209
216
  * them unchanged.
210
217
  *
211
- * Fails at construction with {@link JjError} exactly as {@link layer} does;
212
- * here the `jj --version` probe runs through the contained spawner, so its
218
+ * Builds without jj exactly as {@link layer} does; here the first
219
+ * operation's `jj --version` probe runs through the contained spawner, so its
213
220
  * record enters and leaves the ledger like any other child.
214
221
  *
215
222
  * @category layers
@@ -228,8 +235,7 @@ export const layerContained = (options) => {
228
235
  *
229
236
  * Refuses a root exactly as {@link layerAt} does: {@link BunHostError} with
230
237
  * code `invalid_repository_root`, thrown when the factory is called.
231
- * Fails at construction with {@link JjError} exactly as {@link layerContained}
232
- * does.
238
+ * Builds without jj exactly as {@link layerContained} does.
233
239
  *
234
240
  * @category layers
235
241
  * @since 1.0.0-rc.0
@@ -1 +1 @@
1
- {"version":3,"file":"BunHost.js","sourceRoot":"","sources":["../../src/BunHost.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Aggregate Bun Host bundle.\n *\n * Runtime-specific dependencies stay inside this package; callers get the same\n * closed five-service Host surface every other bundle provides, plus native\n * `ProcessConfinement` for guarded commands.\n *\n * There is no Bun shell module: running a command is Effect's\n * `ChildProcessSpawner`, and `@effect/platform-bun`'s implementation is\n * `@effect/platform-node-shared`'s re-exported: the same code on both\n * runtimes, so no runtime detection is needed here.\n *\n * Outgoing HTTP uses the shared EgressHttpClient on both runtimes. Web\n * requests pin approved addresses; ordinary requests retain the shared pool.\n * Redirects remain visible to the capability kernel, and proxy routing follows\n * the host environment.\n *\n * The filesystem slot is `@smthrs/platform-node`'s `AtomicFileSystem`, byte for\n * byte the layer `NodeHost` uses, so a guarded path operation is\n * descriptor-relative and no-follow on both runtimes.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as BunChildProcessSpawner from \"@effect/platform-bun/BunChildProcessSpawner\"\nimport * as BunCrypto from \"@effect/platform-bun/BunCrypto\"\nimport * as BunPath from \"@effect/platform-bun/BunPath\"\nimport type { Jj, JjError } from \"@smthrs/jj\"\nimport * as BunJj from \"@smthrs/jj/bun/BunJj\"\nimport type { HostServiceIds } from \"@smthrs/kernel/HostServices\"\nimport type * as KernelProcessConfinement from \"@smthrs/kernel/ProcessConfinement\"\nimport type * as ProcessLedger from \"@smthrs/kernel/ProcessLedger\"\nimport * as AtomicFileSystem from \"@smthrs/platform-node/AtomicFileSystem\"\nimport * as EgressHttpClient from \"@smthrs/platform-node/EgressHttpClient\"\nimport * as HostLiveness from \"@smthrs/platform-node/HostLiveness\"\nimport * as ProcessConfinement from \"@smthrs/platform-node/ProcessConfinement\"\nimport * as ProcessReaper from \"@smthrs/platform-node/ProcessReaper\"\nimport type * as Crypto from \"effect/Crypto\"\nimport type { FileSystem } from \"effect/FileSystem\"\nimport type { HttpClient } from \"effect/http/HttpClient\"\nimport * as Layer from \"effect/Layer\"\nimport type * as Path from \"effect/Path\"\nimport type { ChildProcessSpawner } from \"effect/process/ChildProcessSpawner\"\nimport * as Schema from \"effect/Schema\"\nimport { isAbsolute } from \"node:path\"\nimport * as BunFileSystem from \"./BunFileSystem.ts\"\n\n/**\n * Bun platform modules for selectively providing individual services.\n *\n * `AtomicFileSystem` is here for the same reason `NodeHost` re-exports it: it\n * owns the only configuration escape hatch the filesystem slot has, and a Bun\n * program with a different packaged helper path needs to reach\n * `AtomicFileSystem.layerWith` without hand-composing the other four tags.\n *\n * `BunJj` is deliberately absent: it belongs to `@smthrs/jj` and is imported\n * from there, never re-exported here. So is `@effect/platform-bun/BunHttpClient`:\n * its `layer` follows redirects to origins the capability kernel never\n * authorized. The network slot on its own is {@link layerHttpClient}.\n *\n * @category re-exports\n * @since 1.0.0-rc.0\n * @slop\n */\nexport { AtomicFileSystem, BunChildProcessSpawner, BunCrypto, BunFileSystem, HostLiveness, ProcessReaper }\n\n/**\n * The complete closed Host service union provided by Bun.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport type BunHost =\n | FileSystem\n | Path.Path\n | ChildProcessSpawner\n | Jj\n | HttpClient\n | KernelProcessConfinement.ProcessConfinement\n\n/**\n * The stable codes a `BunHost` factory refuses with.\n *\n * A code is the contract a caller branches on; the message beside it is for a\n * person. `invalid_repository_root` is the root handed to {@link layerAt} or\n * {@link layerContainedAt} not being absolute, the empty string included.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type BunHostErrorCode = \"invalid_repository_root\"\n\n/**\n * The refusal a `BunHost` factory throws before it builds a layer.\n *\n * It is thrown rather than failed because a factory is called while a program\n * composes its layers, where there is no fiber to fail yet and a wrong root is\n * a composition mistake, not a runtime outcome. It is this package's class,\n * not the `Jj` adapter's, because a Bun caller composes `BunHost` and should\n * learn nothing about the adapter behind the `Jj` slot from an error message.\n *\n * @category errors\n * @since 1.0.0-rc.0\n */\nexport class BunHostError extends Schema.TaggedError<BunHostError>()(\"@smthrs/platform-bun/BunHostError\", {\n code: Schema.Literal(\"invalid_repository_root\"),\n message: Schema.String\n}) {\n override readonly name = \"BunHostError\"\n}\n\n/** The longest message a factory refuses with, in UTF-16 code units. */\nconst messageLimit = 255\n\n/** How many code points of a refused root the message repeats at most. */\nconst previewLimit = 64\n\n/**\n * A bounded, quoted excerpt of a refused root that fits in `budget` code\n * units of the message.\n *\n * Counted in code points rather than UTF-16 units so a cut never splits a\n * surrogate pair, and passed through `JSON.stringify` so a newline in the root\n * cannot break the log line the message lands on. Each code point is encoded\n * on its own and admitted only while the quoted excerpt plus the length\n * suffix still fit, so a control character that widens to a `\\u0001` escape\n * shortens the excerpt instead of lengthening the message, and a cut never\n * lands inside an escape.\n */\nconst preview = (root: string, budget: number): string => {\n const points = Array.from(root)\n const whole = JSON.stringify(root)\n if (points.length <= previewLimit && whole.length <= budget) return whole\n const suffix = `... (${points.length} characters)`\n const room = budget - suffix.length - 2\n let excerpt = \"\"\n for (const point of points.slice(0, previewLimit)) {\n const encoded = JSON.stringify(point).slice(1, -1)\n if (excerpt.length + encoded.length > room) break\n excerpt += encoded\n }\n return `\"${excerpt}\"${suffix}`\n}\n\n/**\n * Refuses a root the `Jj` adapter underneath would refuse, here and in this\n * package's name.\n *\n * `BunJj.layerAt` and `BunJj.layerSpawnerAt` are `NodeJj`'s, and those throw a\n * bare `TypeError` that names `NodeJj`, carries no code, and echoes the whole\n * string. The predicate here is the same `isAbsolute` the adapter applies, so\n * a root that passes this check cannot reach that throw.\n */\nconst absoluteRoot = (factory: \"layerAt\" | \"layerContainedAt\", root: string): string => {\n if (isAbsolute(root)) return root\n const prefix = `BunHost.${factory} requires an absolute repository root, got `\n throw new BunHostError({\n code: \"invalid_repository_root\",\n message: `${prefix}${preview(root, messageLimit - prefix.length)}`\n })\n}\n\n/**\n * Stable implementation identities keyed by the closed Host service slots.\n *\n * Each value names the module behind that slot in the raw host bundle.\n * Contained POSIX factories replace the spawner with {@link ProcessReaper.layerSpawner}.\n * These are identity\n * tokens rather than import specifiers: the filesystem entry is\n * `@smthrs/platform-node/AtomicFileSystem` because that is the implementation,\n * even though a consumer reaches it through `@smthrs/platform-bun`.\n *\n * Nothing digests them yet. `@smthrs/plan`'s step key carries a `layers`\n * component these values are meant to feed, but no planner derives it from a\n * host bundle today, so changing one invalidates no cached step.\n *\n * The keys are written as literals rather than `HostServiceIds` positions so\n * that reordering the closed list cannot silently pair a slot with another\n * slot's implementation.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const implementationIds: Readonly<Record<(typeof HostServiceIds)[number], string>> = {\n \"effect/FileSystem\": \"@smthrs/platform-node/AtomicFileSystem\",\n \"effect/Path\": \"@effect/platform-bun/BunPath\",\n \"effect/process/ChildProcessSpawner\": \"@effect/platform-bun/BunChildProcessSpawner\",\n \"@smthrs/jj/Jj\": \"@smthrs/jj/bun/BunJj\",\n \"effect/HttpClient\": \"@smthrs/platform-node/EgressHttpClient\"\n}\n\n/**\n * What a caller may configure about containment.\n *\n * `ContainedSpawner.Options.platform` is deliberately not part of it. It\n * decides one thing, whether a command that names no `detached` option gets a\n * process group of its own, and this native host detaches by `process.platform`\n * whatever a record claims. A\n * caller-supplied `\"win32\"` on a POSIX host would therefore record\n * `pgid: null` for a child that really does lead a group, and\n * {@link ProcessReaper.reap} would retire that record as `no-group` and leave\n * the orphan running forever: a durable lie rather than a compile error.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type ContainedOptions = ProcessReaper.SpawnerOptions & ProcessReaper.Options\n\n/** The reaper half of {@link ContainedOptions}. */\nconst reaping = (options?: ContainedOptions): ProcessReaper.Options => ({\n ownerPid: options?.ownerPid,\n system: options?.system\n})\n\n/** The two services `BunChildProcessSpawner` resolves paths and files with. */\nconst platform = Layer.mergeAll(BunFileSystem.layer, BunPath.layer, BunCrypto.layer)\n\n/**\n * The network slot on its own: the shared egress client, which never follows a\n * redirect. A `3xx` comes back to the caller with its `location` intact and the\n * second origin uncontacted; following a redirect is `@smthrs/kernel`'s guarded\n * `HttpClient.layer`, which rechecks every hop.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerHttpClient: Layer.Layer<HttpClient> = EgressHttpClient.layer(process.env)\n\n/**\n * Provides all five Bun Host services, including the runtime-independent Path\n * service.\n *\n * Fails at construction with {@link JjError} carrying `not_installed` or\n * `unsupported_version`: the `Jj` slot is merged eagerly and probes\n * `jj --version` while the layer is built, before the program body runs and\n * whether or not the program asks for `Jj`. Requires jj 0.39.0 or later.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layer: Layer.Layer<BunHost | Crypto.Crypto, JjError> = Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n Layer.provide(BunChildProcessSpawner.layer, platform),\n BunJj.layer,\n layerHttpClient\n)\n\n/**\n * Provides all five Bun Host services bound to one absolute repository root.\n *\n * Throws {@link BunHostError} with code `invalid_repository_root` when `root`\n * is not absolute; the empty string counts. The check runs when the factory is\n * called, before any layer exists.\n *\n * Fails at construction with {@link JjError} exactly as {@link layer} does:\n * the `Jj` slot probes `jj --version` while the layer is built.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerAt = (root: string): Layer.Layer<BunHost | Crypto.Crypto, JjError> => {\n const repositoryRoot = absoluteRoot(\"layerAt\", root)\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n Layer.provide(BunChildProcessSpawner.layer, platform),\n BunJj.layerAt(repositoryRoot),\n layerHttpClient\n )\n}\n\n/**\n * Provides the Bun host with process containment turned on.\n *\n * Bun uses the same prepared native process lifecycle as Node on POSIX, while\n * preserving its underlying runtime spawner on Windows. `ContainedSpawner` gives\n * every child an escalation deadline and a ledger entry, and\n * `ProcessReaper` sweeps the entries a crashed incarnation of this host left\n * behind. The reaper module lives in `@smthrs/platform-node` because the calls\n * it makes, `process.kill` and `taskkill`, are Node's, and Bun implements\n * them unchanged.\n *\n * Fails at construction with {@link JjError} exactly as {@link layer} does;\n * here the `jj --version` probe runs through the contained spawner, so its\n * record enters and leaves the ledger like any other child.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerContained = (\n options?: ContainedOptions\n): Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger> => {\n const spawner = Layer.provide(\n ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),\n Layer.provide(BunChildProcessSpawner.layer, platform)\n )\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n layerHttpClient,\n // jj goes through the CONTAINED spawner here, not around it, exactly as in\n // `NodeHost.layerContained`: a jj child that starts its own process leads\n // no recorded group, is in no ledger, and outlives the host that ran it.\n Layer.provideMerge(BunJj.layerSpawner, spawner)\n ).pipe(Layer.provideMerge(ProcessReaper.layer(reaping(options))))\n}\n\n/**\n * Provides the contained Bun host bound to one absolute repository root.\n *\n * Refuses a root exactly as {@link layerAt} does: {@link BunHostError} with\n * code `invalid_repository_root`, thrown when the factory is called.\n * Fails at construction with {@link JjError} exactly as {@link layerContained}\n * does.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerContainedAt = (\n root: string,\n options?: ContainedOptions\n): Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger> => {\n const repositoryRoot = absoluteRoot(\"layerContainedAt\", root)\n const spawner = Layer.provide(\n ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),\n Layer.provide(BunChildProcessSpawner.layer, platform)\n )\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n layerHttpClient,\n Layer.provideMerge(BunJj.layerSpawnerAt(repositoryRoot), spawner)\n ).pipe(Layer.provideMerge(ProcessReaper.layer(reaping(options))))\n}\n"]}
1
+ {"version":3,"file":"BunHost.js","sourceRoot":"","sources":["../../src/BunHost.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Aggregate Bun Host bundle.\n *\n * Runtime-specific dependencies stay inside this package; callers get the same\n * closed five-service Host surface every other bundle provides, plus native\n * `ProcessConfinement` for guarded commands.\n *\n * There is no Bun shell module: running a command is Effect's\n * `ChildProcessSpawner`, and `@effect/platform-bun`'s implementation is\n * `@effect/platform-node-shared`'s re-exported: the same code on both\n * runtimes, so no runtime detection is needed here.\n *\n * Outgoing HTTP uses the shared EgressHttpClient on both runtimes. Web\n * requests pin approved addresses; ordinary requests retain the shared pool.\n * Redirects remain visible to the capability kernel, and proxy routing follows\n * the host environment.\n *\n * The filesystem slot is `@smthrs/platform-node`'s `AtomicFileSystem`, byte for\n * byte the layer `NodeHost` uses, so a guarded path operation is\n * descriptor-relative and no-follow on both runtimes.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as BunChildProcessSpawner from \"@effect/platform-bun/BunChildProcessSpawner\"\nimport * as BunCrypto from \"@effect/platform-bun/BunCrypto\"\nimport * as BunPath from \"@effect/platform-bun/BunPath\"\nimport type { Jj } from \"@smthrs/jj\"\nimport * as BunJj from \"@smthrs/jj/bun/BunJj\"\nimport type { HostServiceIds } from \"@smthrs/kernel/HostServices\"\nimport type * as KernelProcessConfinement from \"@smthrs/kernel/ProcessConfinement\"\nimport type * as ProcessLedger from \"@smthrs/kernel/ProcessLedger\"\nimport * as AtomicFileSystem from \"@smthrs/platform-node/AtomicFileSystem\"\nimport * as EgressHttpClient from \"@smthrs/platform-node/EgressHttpClient\"\nimport * as HostLiveness from \"@smthrs/platform-node/HostLiveness\"\nimport * as ProcessConfinement from \"@smthrs/platform-node/ProcessConfinement\"\nimport * as ProcessReaper from \"@smthrs/platform-node/ProcessReaper\"\nimport type * as Crypto from \"effect/Crypto\"\nimport type { FileSystem } from \"effect/FileSystem\"\nimport type { HttpClient } from \"effect/http/HttpClient\"\nimport * as Layer from \"effect/Layer\"\nimport type * as Path from \"effect/Path\"\nimport type { ChildProcessSpawner } from \"effect/process/ChildProcessSpawner\"\nimport * as Schema from \"effect/Schema\"\nimport { isAbsolute } from \"node:path\"\nimport * as BunFileSystem from \"./BunFileSystem.ts\"\n\n/**\n * Bun platform modules for selectively providing individual services.\n *\n * `AtomicFileSystem` is here for the same reason `NodeHost` re-exports it: it\n * owns the only configuration escape hatch the filesystem slot has, and a Bun\n * program with a different packaged helper path needs to reach\n * `AtomicFileSystem.layerWith` without hand-composing the other four tags.\n *\n * `BunJj` is deliberately absent: it belongs to `@smthrs/jj` and is imported\n * from there, never re-exported here. So is `@effect/platform-bun/BunHttpClient`:\n * its `layer` follows redirects to origins the capability kernel never\n * authorized. The network slot on its own is {@link layerHttpClient}.\n *\n * @category re-exports\n * @since 1.0.0-rc.0\n * @slop\n */\nexport { AtomicFileSystem, BunChildProcessSpawner, BunCrypto, BunFileSystem, HostLiveness, ProcessReaper }\n\n/**\n * The complete closed Host service union provided by Bun.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport type BunHost =\n | FileSystem\n | Path.Path\n | ChildProcessSpawner\n | Jj\n | HttpClient\n | KernelProcessConfinement.ProcessConfinement\n\n/**\n * The stable codes a `BunHost` factory refuses with.\n *\n * A code is the contract a caller branches on; the message beside it is for a\n * person. `invalid_repository_root` is the root handed to {@link layerAt} or\n * {@link layerContainedAt} not being absolute or containing NUL, the empty\n * string included.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type BunHostErrorCode = \"invalid_repository_root\"\n\n/**\n * The refusal a `BunHost` factory throws before it builds a layer.\n *\n * It is thrown rather than failed because a factory is called while a program\n * composes its layers, where there is no fiber to fail yet and a wrong root is\n * a composition mistake, not a runtime outcome. It is this package's class,\n * not the `Jj` adapter's, because a Bun caller composes `BunHost` and should\n * learn nothing about the adapter behind the `Jj` slot from an error message.\n *\n * @category errors\n * @since 1.0.0-rc.0\n */\nexport class BunHostError extends Schema.TaggedError<BunHostError>()(\"@smthrs/platform-bun/BunHostError\", {\n code: Schema.Literal(\"invalid_repository_root\"),\n message: Schema.String\n}) {\n override readonly name = \"BunHostError\"\n}\n\n/** The longest message a factory refuses with, in UTF-16 code units. */\nconst messageLimit = 255\n\n/** How many code points of a refused root the message repeats at most. */\nconst previewLimit = 64\n\n/**\n * A bounded, quoted excerpt of a refused root that fits in `budget` code\n * units of the message.\n *\n * Counted in code points rather than UTF-16 units so a cut never splits a\n * surrogate pair, and passed through `JSON.stringify` so a newline in the root\n * cannot break the log line the message lands on. Each code point is encoded\n * on its own and admitted only while the quoted excerpt plus the length\n * suffix still fit, so a control character that widens to a `\\u0001` escape\n * shortens the excerpt instead of lengthening the message, and a cut never\n * lands inside an escape.\n */\nconst preview = (root: string, budget: number): string => {\n const points = Array.from(root)\n const whole = JSON.stringify(root)\n if (points.length <= previewLimit && whole.length <= budget) return whole\n const suffix = `... (${points.length} characters)`\n const room = budget - suffix.length - 2\n let excerpt = \"\"\n for (const point of points.slice(0, previewLimit)) {\n const encoded = JSON.stringify(point).slice(1, -1)\n if (excerpt.length + encoded.length > room) break\n excerpt += encoded\n }\n return `\"${excerpt}\"${suffix}`\n}\n\n/**\n * Refuses a root the `Jj` adapter underneath would refuse, here and in this\n * package's name.\n *\n * `BunJj.layerAt` and `BunJj.layerSpawnerAt` are `NodeJj`'s, and those throw a\n * bare `TypeError` that names `NodeJj`, carries no code, and echoes the whole\n * string. The predicate includes the same `isAbsolute` the adapter applies, so\n * a root that passes this check cannot reach that throw. An absolute root\n * containing NUL is refused too: no filesystem or process API can represent it.\n */\nconst absoluteRoot = (factory: \"layerAt\" | \"layerContainedAt\", root: string): string => {\n // An absolute root with NUL names that defect, not absoluteness it already has.\n const requirement = !isAbsolute(root)\n ? \"an absolute repository root\"\n : root.includes(\"\\0\")\n ? \"a repository root without NUL\"\n : undefined\n if (requirement === undefined) return root\n const prefix = `BunHost.${factory} requires ${requirement}, got `\n throw new BunHostError({\n code: \"invalid_repository_root\",\n message: `${prefix}${preview(root, messageLimit - prefix.length)}`\n })\n}\n\n/**\n * Stable implementation identities keyed by the closed Host service slots.\n *\n * Each value names the module behind that slot in the raw host bundle.\n * Contained POSIX factories replace the spawner with {@link ProcessReaper.layerSpawner}.\n * These are identity\n * tokens rather than import specifiers: the filesystem entry is\n * `@smthrs/platform-node/AtomicFileSystem` because that is the implementation,\n * even though a consumer reaches it through `@smthrs/platform-bun`.\n *\n * Nothing digests them yet. `@smthrs/plan`'s step key carries a `layers`\n * component these values are meant to feed, but no planner derives it from a\n * host bundle today, so changing one invalidates no cached step.\n *\n * The keys are written as literals rather than `HostServiceIds` positions so\n * that reordering the closed list cannot silently pair a slot with another\n * slot's implementation.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const implementationIds: Readonly<Record<(typeof HostServiceIds)[number], string>> = {\n \"effect/FileSystem\": \"@smthrs/platform-node/AtomicFileSystem\",\n \"effect/Path\": \"@effect/platform-bun/BunPath\",\n \"effect/process/ChildProcessSpawner\": \"@effect/platform-bun/BunChildProcessSpawner\",\n \"@smthrs/jj/Jj\": \"@smthrs/jj/bun/BunJj\",\n \"effect/HttpClient\": \"@smthrs/platform-node/EgressHttpClient\"\n}\n\n/**\n * What a caller may configure about containment.\n *\n * `ContainedSpawner.Options.platform` is deliberately not part of it. It\n * decides one thing, whether a command that names no `detached` option gets a\n * process group of its own, and this native host detaches by `process.platform`\n * whatever a record claims. A\n * caller-supplied `\"win32\"` on a POSIX host would therefore record\n * `pgid: null` for a child that really does lead a group, and\n * {@link ProcessReaper.reap} would retire that record as `no-group` and leave\n * the orphan running forever: a durable lie rather than a compile error.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type ContainedOptions = ProcessReaper.SpawnerOptions & ProcessReaper.Options\n\n/** The reaper half of {@link ContainedOptions}. */\nconst reaping = (options?: ContainedOptions): ProcessReaper.Options => ({\n ownerPid: options?.ownerPid,\n system: options?.system\n})\n\n/** The two services `BunChildProcessSpawner` resolves paths and files with. */\nconst platform = Layer.mergeAll(BunFileSystem.layer, BunPath.layer, BunCrypto.layer)\n\n/**\n * The network slot on its own: the shared egress client, which never follows a\n * redirect. A `3xx` comes back to the caller with its `location` intact and the\n * second origin uncontacted; following a redirect is `@smthrs/kernel`'s guarded\n * `HttpClient.layer`, which rechecks every hop.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerHttpClient: Layer.Layer<HttpClient> = EgressHttpClient.layer(process.env)\n\n/**\n * Provides all five Bun Host services, including the runtime-independent Path\n * service.\n *\n * Builds on a host without jj. The `Jj` slot probes `jj --version` on its\n * first operation and fails that operation with `JjError` carrying\n * `not_installed` or `unsupported_version`; a program that never calls `Jj`\n * runs without jj. `Jj` requires jj 0.39.0 or later.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layer: Layer.Layer<BunHost | Crypto.Crypto> = Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n Layer.provide(BunChildProcessSpawner.layer, platform),\n BunJj.layer,\n layerHttpClient\n)\n\n/**\n * Provides all five Bun Host services bound to one absolute repository root.\n *\n * Throws {@link BunHostError} with code `invalid_repository_root` when `root`\n * is not absolute or contains NUL; the empty string counts. The check runs\n * when the factory is called, before any layer exists.\n *\n * Builds without jj exactly as {@link layer} does: the `Jj` slot probes\n * `jj --version` on its first operation.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerAt = (root: string): Layer.Layer<BunHost | Crypto.Crypto> => {\n const repositoryRoot = absoluteRoot(\"layerAt\", root)\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n Layer.provide(BunChildProcessSpawner.layer, platform),\n BunJj.layerAt(repositoryRoot),\n layerHttpClient\n )\n}\n\n/**\n * Provides the Bun host with process containment turned on.\n *\n * Bun uses the same prepared native process lifecycle as Node on POSIX, while\n * preserving its underlying runtime spawner on Windows. `ContainedSpawner` gives\n * every child an escalation deadline and a ledger entry, and\n * `ProcessReaper` sweeps the entries a crashed incarnation of this host left\n * behind. The reaper module lives in `@smthrs/platform-node` because the calls\n * it makes, `process.kill` and `taskkill`, are Node's, and Bun implements\n * them unchanged.\n *\n * Builds without jj exactly as {@link layer} does; here the first\n * operation's `jj --version` probe runs through the contained spawner, so its\n * record enters and leaves the ledger like any other child.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerContained = (\n options?: ContainedOptions\n): Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger> => {\n const spawner = Layer.provide(\n ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),\n Layer.provide(BunChildProcessSpawner.layer, platform)\n )\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n layerHttpClient,\n // jj goes through the CONTAINED spawner here, not around it, exactly as in\n // `NodeHost.layerContained`: a jj child that starts its own process leads\n // no recorded group, is in no ledger, and outlives the host that ran it.\n Layer.provideMerge(BunJj.layerSpawner, spawner)\n ).pipe(Layer.provideMerge(ProcessReaper.layer(reaping(options))))\n}\n\n/**\n * Provides the contained Bun host bound to one absolute repository root.\n *\n * Refuses a root exactly as {@link layerAt} does: {@link BunHostError} with\n * code `invalid_repository_root`, thrown when the factory is called.\n * Builds without jj exactly as {@link layerContained} does.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerContainedAt = (\n root: string,\n options?: ContainedOptions\n): Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger> => {\n const repositoryRoot = absoluteRoot(\"layerContainedAt\", root)\n const spawner = Layer.provide(\n ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),\n Layer.provide(BunChildProcessSpawner.layer, platform)\n )\n return Layer.mergeAll(\n ProcessConfinement.layer(),\n platform,\n layerHttpClient,\n Layer.provideMerge(BunJj.layerSpawnerAt(repositoryRoot), spawner)\n ).pipe(Layer.provideMerge(ProcessReaper.layer(reaping(options))))\n}\n"]}
package/docs/README.md CHANGED
@@ -38,15 +38,15 @@ is Effect's Node spawner re-exported; contained POSIX variants use
38
38
  ## Install
39
39
 
40
40
  ```bash
41
- bun add @smthrs/platform-bun@1.0.0-rc.8 effect
41
+ bun add @smthrs/platform-bun@1.0.0-rc.9 effect
42
42
  ```
43
43
 
44
44
  `effect` is a required peer in the range `^4.0.4`. `@effect/platform-bun` is
45
45
  bundled privately, built from `effect@4.0.4`. The filesystem slot runs the
46
46
  `smithers-jj-export` helper that the required peer `@smthrs/platform-node`
47
- ships, and every complete bundle needs jj 0.39.0 or later because it probes jj
48
- while the layer is built. [Installation](./installation.md) covers all three
49
- and the import forms.
47
+ ships. The `Jj` slot needs jj 0.39.0 or later, checked on its first operation;
48
+ every bundle builds without jj. [Installation](./installation.md) covers all
49
+ three and the import forms.
50
50
 
51
51
  ## Run a command and a file operation through the host
52
52
 
package/docs/api.md CHANGED
@@ -27,13 +27,13 @@ a browser; a page composes
27
27
  [`@smthrs/platform-browser`](/api/platform-browser) instead.
28
28
  :::
29
29
 
30
- The complete host bundles require jj 0.39.0 or newer. Each bundle checks its jj
31
- executable before exposing repository operations. Construction can fail with
32
- `JjError`, including `not_installed` or `unsupported_version`. The contained
33
- bundles route that probe
34
- through their process spawner and retire its ledger entry when it exits. Probe
35
- results are cached per resolved absolute executable path and runner; contained
36
- spawners share a result only within the same spawner instance.
30
+ Every host bundle builds without jj. The `Jj` slot requires jj 0.39.0 or newer
31
+ and checks its executable on its first operation, which fails with `JjError`
32
+ `not_installed` or `unsupported_version` when jj is missing or older. The
33
+ contained bundles route that probe through their process spawner and retire its
34
+ ledger entry when it exits. Probe results are cached per resolved absolute
35
+ executable path and runner; contained spawners share a result only within the
36
+ same spawner instance.
37
37
 
38
38
  ## Entry points
39
39
 
@@ -52,24 +52,24 @@ Supported runtimes are Bun >=1.4.0 and Node.js >=26.4.0.
52
52
 
53
53
  ## BunHost
54
54
 
55
- The complete closed Host bundle: layers, construction errors, models,
55
+ The complete closed Host bundle: layers, root errors, models,
56
56
  implementation identities, and platform module re-exports.
57
57
 
58
58
  ### Layers
59
59
 
60
- | Export | Signature |
61
- | ------------------ | ------------------------------------------------------------------------------------------------------------- |
62
- | `layer` | `Layer.Layer<BunHost \| Crypto.Crypto, JjError>` |
63
- | `layerAt` | `(root: string) => Layer.Layer<BunHost \| Crypto.Crypto, JjError>` |
64
- | `layerContained` | `(options?: ContainedOptions) => Layer.Layer<BunHost \| Crypto.Crypto, JjError, ProcessLedger>` |
65
- | `layerContainedAt` | `(root: string, options?: ContainedOptions) => Layer.Layer<BunHost \| Crypto.Crypto, JjError, ProcessLedger>` |
66
- | `layerHttpClient` | `Layer.Layer<HttpClient>` |
60
+ | Export | Signature |
61
+ | ------------------ | ----------------------------------------------------------------------------------------------------------- |
62
+ | `layer` | `Layer.Layer<BunHost \| Crypto.Crypto>` |
63
+ | `layerAt` | `(root: string) => Layer.Layer<BunHost \| Crypto.Crypto>` |
64
+ | `layerContained` | `(options?: ContainedOptions) => Layer.Layer<BunHost \| Crypto.Crypto, never, ProcessLedger>` |
65
+ | `layerContainedAt` | `(root: string, options?: ContainedOptions) => Layer.Layer<BunHost \| Crypto.Crypto, never, ProcessLedger>` |
66
+ | `layerHttpClient` | `Layer.Layer<HttpClient>` |
67
67
 
68
68
  `layer` provides all five Host services, including the runtime-independent
69
69
  `Path`. `Jj` is bound to the process working directory.
70
70
 
71
71
  `layerAt` is `layer` with `Jj` bound to `root` instead. `root` must be an
72
- absolute path; see [BunHostError](#bunhosterror).
72
+ absolute path without NUL; see [BunHostError](#bunhosterror).
73
73
 
74
74
  `layerContained` is `layer` with process containment turned on: every child is
75
75
  spawned through `@smthrs/kernel`'s `ContainedSpawner` with
@@ -117,10 +117,10 @@ type BunHostErrorCode = "invalid_repository_root"
117
117
  ```
118
118
 
119
119
  The refusal `layerAt` and `layerContainedAt` throw when `root` is not absolute,
120
- the empty string included. It is thrown rather than failed because a factory
121
- runs while a program composes its layers, where there is no fiber to fail into
122
- and a wrong root is a composition mistake, not a runtime outcome. The check
123
- runs when the factory is called, before any layer exists.
120
+ the empty string included, or contains NUL. It is thrown rather than failed
121
+ because a factory runs while a program composes its layers, where there is no
122
+ fiber to fail into and a wrong root is a composition mistake, not a runtime
123
+ outcome. The check runs when the factory is called, before any layer exists.
124
124
 
125
125
  It is this package's class rather than the `Jj` adapter's: a Bun caller
126
126
  composed `BunHost` and should learn nothing about the adapter behind the `Jj`
@@ -42,8 +42,8 @@ BunHost.layerContainedAt("/srv/repositories/app", { graceMs: 2_000 })
42
42
 
43
43
  ## Pass an absolute root
44
44
 
45
- Both factories require an absolute path. A relative root, and the empty string,
46
- are refused:
45
+ Both factories require an absolute path without NUL. A relative root, the
46
+ empty string, and an absolute root containing NUL are refused:
47
47
 
48
48
  ```ts
49
49
  BunHost.layerAt("repositories/app")
@@ -130,15 +130,16 @@ const status = Effect.flatMap(Jj, (jj) => jj.status()).pipe(
130
130
  ```
131
131
 
132
132
  That is the observable difference: the same call under `BunHost.layer` leaves
133
- the ledger empty. The initial `jj --version` probe uses the selected runner
134
- too, so a contained host records and retires it before exposing `Jj`. Probe
135
- results are cached per resolved absolute executable path and runner, with a
136
- separate cache for each spawner instance.
137
-
138
- Every aggregate host waits up to 5000 ms for the startup probe, then interrupts
139
- it and awaits process cleanup. A probe that misses this budget, or that the
133
+ the ledger empty. The `jj --version` probe that the first `Jj` operation runs
134
+ uses the selected runner too, so a contained host records and retires it like
135
+ any other child. Building the host spawns nothing. Probe results are cached per
136
+ resolved absolute executable path and runner, with a separate cache for each
137
+ spawner instance.
138
+
139
+ The first `Jj` operation on every aggregate host waits up to 5000 ms for the
140
+ version probe, then interrupts it and awaits process cleanup. A probe that misses this budget, or that the
140
141
  spawner was too busy to start in time, is retried once. A second timeout fails
141
- with a typed `JjError` that has `method: "version"`, `code: "unknown"`,
142
+ the operation with a typed `JjError` that has `method: "version"`, `code: "unknown"`,
142
143
  `cause.code: "ETIMEDOUT"`, and the executable path in its command and message.
143
144
  Cleanup can add the contained spawner's shutdown grace period to each attempt. Raw hosts close the probe's pipes and kill its
144
145
  direct child; they do not add a ledger entry.
@@ -154,7 +155,7 @@ const boundedHost = host.pipe(
154
155
  ```
155
156
 
156
157
  The budget is a positive, finite number of milliseconds, at most 2147483647.
157
- Timeout and interruption allow a later construction to retry the probe.
158
+ Timeout and interruption allow the next operation to retry the probe.
158
159
 
159
160
  ## What the reaper refuses to kill
160
161
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Installation"
3
- description: "Install @smthrs/platform-bun from npm with its required peers, the smithers-jj-export helper @smthrs/platform-node ships for the filesystem slot, the jj executable every complete bundle probes at startup, and the import forms for each entry point."
3
+ description: "Install @smthrs/platform-bun from npm with its required peers, the smithers-jj-export helper @smthrs/platform-node ships for the filesystem slot, the jj executable the Jj slot probes on first use, and the import forms for each entry point."
4
4
  sidebar:
5
5
  order: 1
6
6
  ---
@@ -11,9 +11,13 @@ The package is on npm under the `next` tag, which carries the 1.0 release
11
11
  candidates. Install the current candidate with Bun:
12
12
 
13
13
  ```bash
14
- bun add @smthrs/platform-bun@1.0.0-rc.8 effect
14
+ bun add @smthrs/platform-bun@1.0.0-rc.9 effect @smthrs/kernel@1.0.0-rc.9
15
15
  ```
16
16
 
17
+ `@smthrs/kernel` is on the line because the [Quickstart](./quickstart.md)'s
18
+ `contained.ts` imports its `ProcessLedger`, which the contained bundles require.
19
+ Bun's isolated linker and pnpm link only the packages a project declares.
20
+
17
21
  The manifest declares two required peers, neither optional:
18
22
 
19
23
  | Peer | Version | Why |
@@ -51,15 +55,15 @@ packaged helper with another absolute path; see
51
55
 
52
56
  Windows is unsupported for this slot.
53
57
 
54
- ## Install Jujutsu for the complete bundles
58
+ ## Install Jujutsu for the Jj slot
55
59
 
56
- Every complete bundle, `BunHost.layer`, `layerAt`, `layerContained`, and
57
- `layerContainedAt`, requires jj 0.39.0 or later on the host, whether or not
58
- the program uses the `Jj` slot. Each factory merges its `Jj` layer with the
59
- other four, and that layer runs one `jj --version` probe while the layer is
60
- built. On a host without the executable, or with an older one, construction
61
- fails with `JjError` carrying `not_installed` or `unsupported_version` before
62
- the program body runs, even for a program that asked for only `FileSystem`.
60
+ Every bundle, `BunHost.layer`, `layerAt`, `layerContained`, and
61
+ `layerContainedAt`, builds without jj, so a program that never calls `Jj` runs
62
+ on a host without it. The `Jj` slot requires jj 0.39.0 or later: its first
63
+ operation runs one `jj --version` probe, and on a host without the executable,
64
+ or with an older one, that operation fails with `JjError` carrying
65
+ `not_installed` or `unsupported_version`. The slot selects the executable when
66
+ the layer is built, so restart the program after installing jj.
63
67
 
64
68
  Install [Jujutsu](https://jj-vcs.github.io), a version-control system that
65
69
  works on a Git repository, and confirm the version:
@@ -72,9 +76,9 @@ This package vendors no binaries. [`@smthrs/jj`](/api/jj) documents the
72
76
  resolution order and the `SMITHERS_JJ_PATH` override for an executable that is
73
77
  not on `PATH`.
74
78
 
75
- A program that must run without jj composes the individual service layers
76
- instead of a complete bundle: `BunFileSystem.layer`, `Path.layer`, and the
77
- spawner and HTTP client layers each build without a probe. See
79
+ A program that wants only some of the services composes the individual service
80
+ layers instead of a complete bundle: `BunFileSystem.layer`, `Path.layer`, and
81
+ the spawner and HTTP client layers. See
78
82
  [The Host surface on Bun](./concepts/host-surface.md) for taking one service
79
83
  without the other four.
80
84
 
@@ -117,9 +121,9 @@ page composes [`@smthrs/platform-browser`](/api/platform-browser) instead.
117
121
  [Contain and reap child processes](./guides/contain-child-processes.md).
118
122
 
119
123
  The `Jj` slot spawns the `jj` command that
120
- [Install Jujutsu for the complete bundles](#install-jujutsu-for-the-complete-bundles)
121
- covers. A bundle probes it at construction, so the executable is a startup
122
- requirement rather than a slot the program opts into.
124
+ [Install Jujutsu for the Jj slot](#install-jujutsu-for-the-jj-slot) covers. The
125
+ slot probes it on its first operation, so jj is a requirement of the programs
126
+ that call `Jj`, not of every host.
123
127
 
124
128
  ## Next step
125
129
 
@@ -13,16 +13,18 @@ appear in, and disappear from, the process ledger.
13
13
  ## Prerequisites
14
14
 
15
15
  - Bun 1.4.0 or later, or Node.js 26.4.0 or later.
16
- - A project with the package installed:
17
- `bun add @smthrs/platform-bun@1.0.0-rc.8 effect`. That also installs the
18
- required peer `@smthrs/platform-node`, which ships the `smithers-jj-export`
19
- helper the filesystem slot runs. See [Installation](./installation.md).
20
- - [Jujutsu](https://jj-vcs.github.io) at jj 0.39.0 or later on `PATH`. Confirm
21
- it with `jj --version`. Neither program below touches `Jj`, but every
22
- complete bundle probes jj while the layer is built and fails with `JjError`
23
- before the program body runs when it is missing. See
24
- [Installation](./installation.md) for `SMITHERS_JJ_PATH` and for composing
25
- service layers without jj.
16
+ - A project with the packages installed:
17
+ `bun add @smthrs/platform-bun@1.0.0-rc.9 effect @smthrs/kernel@1.0.0-rc.9`.
18
+ That also installs the required peer `@smthrs/platform-node`, which ships the
19
+ `smithers-jj-export` helper the filesystem slot runs. `@smthrs/kernel` is on
20
+ the line because `contained.ts` imports its `ProcessLedger`, and Bun's
21
+ isolated linker and pnpm link only the packages a project declares. See
22
+ [Installation](./installation.md).
23
+ - [Jujutsu](https://jj-vcs.github.io) at jj 0.39.0 or later on `PATH`, only for
24
+ a program that calls `Jj`. Neither program below does, and every bundle
25
+ builds without jj; a `Jj` operation on a host without it fails with
26
+ `JjError` `not_installed`. See [Installation](./installation.md) for
27
+ `SMITHERS_JJ_PATH`.
26
28
 
27
29
  ## Run a file operation and a command
28
30
 
@@ -64,8 +66,9 @@ bun run quickstart.ts
64
66
  ```
65
67
 
66
68
  `BunHost.layer` provided all five slots; the program used two of them. The
67
- other three were built and never asked for, and building `Jj` is where the
68
- jj version probe ran.
69
+ other three were built and never asked for. Building `Jj` spawned nothing: its
70
+ jj version probe runs on the first `Jj` operation, which this program never
71
+ makes.
69
72
 
70
73
  ## Turn containment on
71
74
 
@@ -1,33 +1,12 @@
1
1
  ---
2
2
  title: "Troubleshooting"
3
- description: "The failures @smthrs/platform-bun produces: a missing required peer, a refused repository root, a filesystem guard that fails closed, an unfollowed redirect, a missing process ledger, and a browser bundle that will not resolve."
3
+ description: "The failures @smthrs/platform-bun produces: a refused repository root, a filesystem guard that fails closed, an unfollowed redirect, a missing process ledger, and a browser bundle that will not resolve."
4
4
  ---
5
5
 
6
6
  Find the symptom, read the cause, apply the fix. Every failure here is one this
7
7
  package or its slots actually produce; the signatures behind them are on the
8
8
  [API reference](./api.md).
9
9
 
10
- ## ERR_MODULE_NOT_FOUND for @effect/platform-bun/BunChildProcessSpawner
11
-
12
- **Symptom.** The first `import { BunHost } from "@smthrs/platform-bun"`, or
13
- `import * as BunHost from "@smthrs/platform-bun/BunHost"`, throws
14
- `ERR_MODULE_NOT_FOUND` before any of your code runs.
15
-
16
- **Cause.** The install is missing the required `@effect/platform-bun` peer,
17
- which both the root entry point and `BunHost` import at module load. This can
18
- happen with an older package version that marked it optional, or when peer
19
- installation was disabled.
20
-
21
- **Fix.** Install it yourself, at the version your composition's `effect` is
22
- built against:
23
-
24
- ```bash
25
- pnpm add @effect/platform-bun @effect/platform-node-shared
26
- ```
27
-
28
- `@smthrs/platform-bun/BunFileSystem` is the one entry point that resolves
29
- without it, because it imports nothing from `@effect/platform-bun`.
30
-
31
10
  ## BunHostError: requires an absolute repository root
32
11
 
33
12
  **Symptom.** `BunHost.layerAt` or `BunHost.layerContainedAt` throws while your
@@ -35,10 +14,12 @@ program is still composing its layers:
35
14
 
36
15
  ```text
37
16
  BunHostError: BunHost.layerAt requires an absolute repository root, got "repositories/app"
17
+ BunHostError: BunHost.layerAt requires a repository root without NUL, got "/repo\u0000"
38
18
  ```
39
19
 
40
- **Cause.** The root is not absolute. The empty string counts, and so does a
41
- path that looks absolute only after the process working directory is applied.
20
+ **Cause.** The root is not absolute, or it contains NUL, which no filesystem
21
+ or process API can represent. The empty string is not absolute, and neither is
22
+ a path that looks absolute only after the process working directory is applied.
42
23
 
43
24
  **Fix.** Resolve the root before you hand it over, and branch on
44
25
  `error.code === "invalid_repository_root"` rather than on the message text. The
@@ -115,7 +96,7 @@ your code.
115
96
  `@smthrs/kernel/ProcessLedger`.
116
97
 
117
98
  **Cause.** Both contained factories return
118
- `Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger>`.
99
+ `Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger>`.
119
100
  The ledger is a
120
101
  requirement rather than a built-in default, because the durable half is only as
121
102
  good as the journal underneath it, and only your program knows whether it has
@@ -167,15 +148,16 @@ between them does not change. See
167
148
 
168
149
  ## Jj fails with not_installed
169
150
 
170
- **Symptom.** Building any complete bundle fails with a `JjError` whose code
171
- is `not_installed` or `unsupported_version` before the program body runs,
172
- even when the program never asks for `Jj`.
151
+ **Symptom.** A `Jj` operation fails with a `JjError` whose code is
152
+ `not_installed` or `unsupported_version` and whose method is `version`. The
153
+ host built and every other service works.
173
154
 
174
- **Cause.** No usable `jj` executable, or one older than 0.39.0. Every
175
- `BunHost` factory builds its `Jj` layer eagerly, and that layer probes
176
- `jj --version` at construction. This package vendors no binaries and installs
177
- nothing on your behalf.
155
+ **Cause.** No usable `jj` executable, or one older than 0.39.0. The `Jj` slot
156
+ probes `jj --version` on its first operation, and the operation that triggered
157
+ the probe did not run. This package vendors no binaries and installs nothing on
158
+ your behalf.
178
159
 
179
160
  **Fix.** Install [Jujutsu](https://jj-vcs.github.io), which provides the `jj`
180
- command, or set `SMITHERS_JJ_PATH` to the executable you want spawned.
161
+ command, or set `SMITHERS_JJ_PATH` to the executable you want spawned, then
162
+ restart the program: the slot selects the executable when the layer is built.
181
163
  [`@smthrs/jj`](/api/jj) documents the resolution order and the failure codes.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@smthrs/platform-bun",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.8",
4
+ "version": "1.0.0-rc.9",
5
5
  "license": "MIT",
6
6
  "smthrs": {
7
7
  "group": "engine",
@@ -102,15 +102,15 @@
102
102
  "tag": "next"
103
103
  },
104
104
  "dependencies": {
105
- "@smthrs/jj": "1.0.0-rc.8",
106
- "@smthrs/kernel": "1.0.0-rc.8",
105
+ "@smthrs/jj": "1.0.0-rc.9",
106
+ "@smthrs/kernel": "1.0.0-rc.9",
107
107
  "undici": "8.11.2",
108
108
  "redis": "6.2.1",
109
109
  "@types/ws": "8.18.2",
110
110
  "ws": "8.22.0"
111
111
  },
112
112
  "peerDependencies": {
113
- "@smthrs/platform-node": "1.0.0-rc.8",
113
+ "@smthrs/platform-node": "1.0.0-rc.9",
114
114
  "effect": "^4.0.4"
115
115
  },
116
116
  "devDependencies": {
@@ -120,7 +120,7 @@
120
120
  "@effect/platform-node-shared": "4.0.4",
121
121
  "@effect/vitest": "4.0.4",
122
122
  "@eslint/js": "9.39.5",
123
- "@smthrs/platform-node": "1.0.0-rc.8",
123
+ "@smthrs/platform-node": "1.0.0-rc.9",
124
124
  "@types/node": "26.4.1",
125
125
  "@vitest/coverage-v8": "5.0.0",
126
126
  "dprint": "0.57.1",
@@ -135,7 +135,7 @@
135
135
  "typescript": "5.9.3",
136
136
  "typescript-eslint": "8.69.0",
137
137
  "vitest": "5.0.0",
138
- "@smthrs/artifacts": "1.0.0-rc.8"
138
+ "@smthrs/artifacts": "1.0.0-rc.9"
139
139
  },
140
140
  "scripts": {
141
141
  "lint": "eslint src --max-warnings=0 && dprint check",
package/src/BunHost.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  import * as BunChildProcessSpawner from "@effect/platform-bun/BunChildProcessSpawner"
26
26
  import * as BunCrypto from "@effect/platform-bun/BunCrypto"
27
27
  import * as BunPath from "@effect/platform-bun/BunPath"
28
- import type { Jj, JjError } from "@smthrs/jj"
28
+ import type { Jj } from "@smthrs/jj"
29
29
  import * as BunJj from "@smthrs/jj/bun/BunJj"
30
30
  import type { HostServiceIds } from "@smthrs/kernel/HostServices"
31
31
  import type * as KernelProcessConfinement from "@smthrs/kernel/ProcessConfinement"
@@ -84,7 +84,8 @@ export type BunHost =
84
84
  *
85
85
  * A code is the contract a caller branches on; the message beside it is for a
86
86
  * person. `invalid_repository_root` is the root handed to {@link layerAt} or
87
- * {@link layerContainedAt} not being absolute, the empty string included.
87
+ * {@link layerContainedAt} not being absolute or containing NUL, the empty
88
+ * string included.
88
89
  *
89
90
  * @category models
90
91
  * @since 1.0.0-rc.0
@@ -149,12 +150,19 @@ const preview = (root: string, budget: number): string => {
149
150
  *
150
151
  * `BunJj.layerAt` and `BunJj.layerSpawnerAt` are `NodeJj`'s, and those throw a
151
152
  * bare `TypeError` that names `NodeJj`, carries no code, and echoes the whole
152
- * string. The predicate here is the same `isAbsolute` the adapter applies, so
153
- * a root that passes this check cannot reach that throw.
153
+ * string. The predicate includes the same `isAbsolute` the adapter applies, so
154
+ * a root that passes this check cannot reach that throw. An absolute root
155
+ * containing NUL is refused too: no filesystem or process API can represent it.
154
156
  */
155
157
  const absoluteRoot = (factory: "layerAt" | "layerContainedAt", root: string): string => {
156
- if (isAbsolute(root)) return root
157
- const prefix = `BunHost.${factory} requires an absolute repository root, got `
158
+ // An absolute root with NUL names that defect, not absoluteness it already has.
159
+ const requirement = !isAbsolute(root)
160
+ ? "an absolute repository root"
161
+ : root.includes("\0")
162
+ ? "a repository root without NUL"
163
+ : undefined
164
+ if (requirement === undefined) return root
165
+ const prefix = `BunHost.${factory} requires ${requirement}, got `
158
166
  throw new BunHostError({
159
167
  code: "invalid_repository_root",
160
168
  message: `${prefix}${preview(root, messageLimit - prefix.length)}`
@@ -232,16 +240,16 @@ export const layerHttpClient: Layer.Layer<HttpClient> = EgressHttpClient.layer(p
232
240
  * Provides all five Bun Host services, including the runtime-independent Path
233
241
  * service.
234
242
  *
235
- * Fails at construction with {@link JjError} carrying `not_installed` or
236
- * `unsupported_version`: the `Jj` slot is merged eagerly and probes
237
- * `jj --version` while the layer is built, before the program body runs and
238
- * whether or not the program asks for `Jj`. Requires jj 0.39.0 or later.
243
+ * Builds on a host without jj. The `Jj` slot probes `jj --version` on its
244
+ * first operation and fails that operation with `JjError` carrying
245
+ * `not_installed` or `unsupported_version`; a program that never calls `Jj`
246
+ * runs without jj. `Jj` requires jj 0.39.0 or later.
239
247
  *
240
248
  * @category layers
241
249
  * @since 1.0.0-rc.0
242
250
  * @slop
243
251
  */
244
- export const layer: Layer.Layer<BunHost | Crypto.Crypto, JjError> = Layer.mergeAll(
252
+ export const layer: Layer.Layer<BunHost | Crypto.Crypto> = Layer.mergeAll(
245
253
  ProcessConfinement.layer(),
246
254
  platform,
247
255
  Layer.provide(BunChildProcessSpawner.layer, platform),
@@ -253,16 +261,16 @@ export const layer: Layer.Layer<BunHost | Crypto.Crypto, JjError> = Layer.mergeA
253
261
  * Provides all five Bun Host services bound to one absolute repository root.
254
262
  *
255
263
  * Throws {@link BunHostError} with code `invalid_repository_root` when `root`
256
- * is not absolute; the empty string counts. The check runs when the factory is
257
- * called, before any layer exists.
264
+ * is not absolute or contains NUL; the empty string counts. The check runs
265
+ * when the factory is called, before any layer exists.
258
266
  *
259
- * Fails at construction with {@link JjError} exactly as {@link layer} does:
260
- * the `Jj` slot probes `jj --version` while the layer is built.
267
+ * Builds without jj exactly as {@link layer} does: the `Jj` slot probes
268
+ * `jj --version` on its first operation.
261
269
  *
262
270
  * @category layers
263
271
  * @since 1.0.0-rc.0
264
272
  */
265
- export const layerAt = (root: string): Layer.Layer<BunHost | Crypto.Crypto, JjError> => {
273
+ export const layerAt = (root: string): Layer.Layer<BunHost | Crypto.Crypto> => {
266
274
  const repositoryRoot = absoluteRoot("layerAt", root)
267
275
  return Layer.mergeAll(
268
276
  ProcessConfinement.layer(),
@@ -284,8 +292,8 @@ export const layerAt = (root: string): Layer.Layer<BunHost | Crypto.Crypto, JjEr
284
292
  * it makes, `process.kill` and `taskkill`, are Node's, and Bun implements
285
293
  * them unchanged.
286
294
  *
287
- * Fails at construction with {@link JjError} exactly as {@link layer} does;
288
- * here the `jj --version` probe runs through the contained spawner, so its
295
+ * Builds without jj exactly as {@link layer} does; here the first
296
+ * operation's `jj --version` probe runs through the contained spawner, so its
289
297
  * record enters and leaves the ledger like any other child.
290
298
  *
291
299
  * @category layers
@@ -293,7 +301,7 @@ export const layerAt = (root: string): Layer.Layer<BunHost | Crypto.Crypto, JjEr
293
301
  */
294
302
  export const layerContained = (
295
303
  options?: ContainedOptions
296
- ): Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger> => {
304
+ ): Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger> => {
297
305
  const spawner = Layer.provide(
298
306
  ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),
299
307
  Layer.provide(BunChildProcessSpawner.layer, platform)
@@ -314,8 +322,7 @@ export const layerContained = (
314
322
  *
315
323
  * Refuses a root exactly as {@link layerAt} does: {@link BunHostError} with
316
324
  * code `invalid_repository_root`, thrown when the factory is called.
317
- * Fails at construction with {@link JjError} exactly as {@link layerContained}
318
- * does.
325
+ * Builds without jj exactly as {@link layerContained} does.
319
326
  *
320
327
  * @category layers
321
328
  * @since 1.0.0-rc.0
@@ -323,7 +330,7 @@ export const layerContained = (
323
330
  export const layerContainedAt = (
324
331
  root: string,
325
332
  options?: ContainedOptions
326
- ): Layer.Layer<BunHost | Crypto.Crypto, JjError, ProcessLedger.ProcessLedger> => {
333
+ ): Layer.Layer<BunHost | Crypto.Crypto, never, ProcessLedger.ProcessLedger> => {
327
334
  const repositoryRoot = absoluteRoot("layerContainedAt", root)
328
335
  const spawner = Layer.provide(
329
336
  ProcessReaper.layerSpawner({ graceMs: options?.graceMs }),