@nimbus-sh/core 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +47 -26
  2. package/dist/_shared/byte-stream.d.ts +5 -4
  3. package/dist/_shared/byte-stream.d.ts.map +1 -1
  4. package/dist/_shared/real-node-imports.d.ts +7 -0
  5. package/dist/_shared/real-node-imports.d.ts.map +1 -1
  6. package/dist/_shared/real-node-imports.js +8 -0
  7. package/dist/_shared/tarball.d.ts +24 -5
  8. package/dist/_shared/tarball.d.ts.map +1 -1
  9. package/dist/_shared/tarball.js +42 -5
  10. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
  11. package/dist/_shared/vfs-write-ledger.js +22 -16
  12. package/dist/runtime/comment-strip.d.ts +32 -29
  13. package/dist/runtime/comment-strip.d.ts.map +1 -1
  14. package/dist/runtime/comment-strip.js +273 -51
  15. package/dist/runtime/cpython-runner.d.ts +1 -0
  16. package/dist/runtime/cpython-runner.d.ts.map +1 -1
  17. package/dist/runtime/cpython-runner.js +1 -1
  18. package/dist/runtime/esbuild-service.d.ts +38 -42
  19. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  20. package/dist/runtime/esbuild-service.js +500 -388
  21. package/dist/runtime/javascript-ast.d.ts.map +1 -1
  22. package/dist/runtime/javascript-ast.js +45 -7
  23. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
  24. package/dist/runtime/opentui-wasm-backend.js +18 -16
  25. package/dist/runtime/port-registry.d.ts +4 -2
  26. package/dist/runtime/port-registry.d.ts.map +1 -1
  27. package/dist/runtime/port-registry.js +60 -8
  28. package/dist/runtime/ruby-runner.d.ts +1 -0
  29. package/dist/runtime/ruby-runner.d.ts.map +1 -1
  30. package/dist/runtime/ruby-runner.js +1 -0
  31. package/dist/shell/stdin-adapter.d.ts +11 -0
  32. package/dist/shell/stdin-adapter.d.ts.map +1 -0
  33. package/dist/shell/stdin-adapter.js +59 -0
  34. package/dist/shell/unix-commands.d.ts +2 -1
  35. package/dist/shell/unix-commands.d.ts.map +1 -1
  36. package/dist/shell/unix-commands.js +1078 -50
  37. package/dist/substrate/lifo/commands/io/dd.js +1 -1
  38. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
  39. package/dist/substrate/lifo/commands/net/curl.js +270 -54
  40. package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
  41. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
  42. package/dist/substrate/lifo/commands/system/npm.js +25 -43
  43. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
  44. package/dist/substrate/lifo/commands/text/head.js +2 -1
  45. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
  46. package/dist/substrate/lifo/commands/text/sed.js +499 -117
  47. package/dist/substrate/lifo/commands/types.d.ts +8 -4
  48. package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
  49. package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
  50. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
  51. package/dist/substrate/lifo/node-compat/zlib.js +122 -23
  52. package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
  53. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
  54. package/dist/substrate/lifo/shell/interpreter.js +79 -29
  55. package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
  56. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
  57. package/dist/substrate/lifo/shell/pipe.js +62 -18
  58. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
  59. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
  60. package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
  61. package/dist/vfs/sqlite-vfs.d.ts +19 -0
  62. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  63. package/dist/vfs/sqlite-vfs.js +106 -23
  64. package/dist/workspace/nimbus-workspace.d.ts +22 -1
  65. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  66. package/dist/workspace/nimbus-workspace.js +19 -3
  67. package/dist/workspace/supervisor-op.d.ts +107 -0
  68. package/dist/workspace/supervisor-op.d.ts.map +1 -0
  69. package/dist/workspace/supervisor-op.js +224 -0
  70. package/package.json +2 -2
  71. package/src/_shared/byte-stream.ts +5 -4
  72. package/src/_shared/real-node-imports.ts +8 -0
  73. package/src/_shared/tarball.ts +60 -5
  74. package/src/_shared/vfs-write-ledger.ts +22 -16
  75. package/src/runtime/comment-strip.ts +218 -52
  76. package/src/runtime/cpython-runner.ts +2 -1
  77. package/src/runtime/esbuild-service.ts +572 -372
  78. package/src/runtime/javascript-ast.ts +40 -9
  79. package/src/runtime/opentui-wasm-backend.ts +19 -17
  80. package/src/runtime/port-registry.ts +66 -8
  81. package/src/runtime/ruby-runner.ts +2 -0
  82. package/src/shell/stdin-adapter.ts +58 -0
  83. package/src/shell/unix-commands.ts +1054 -50
  84. package/src/substrate/lifo/commands/io/dd.ts +2 -2
  85. package/src/substrate/lifo/commands/net/curl.ts +310 -58
  86. package/src/substrate/lifo/commands/system/npm.ts +55 -38
  87. package/src/substrate/lifo/commands/text/head.ts +2 -1
  88. package/src/substrate/lifo/commands/text/sed.ts +519 -109
  89. package/src/substrate/lifo/commands/types.ts +8 -5
  90. package/src/substrate/lifo/node-compat/zlib.ts +155 -26
  91. package/src/substrate/lifo/shell/interpreter.ts +74 -27
  92. package/src/substrate/lifo/shell/pipe.ts +65 -24
  93. package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
  94. package/src/vfs/sqlite-vfs.ts +104 -23
  95. package/src/workspace/nimbus-workspace.ts +30 -3
  96. package/src/workspace/supervisor-op.ts +309 -0
package/README.md CHANGED
@@ -5,16 +5,23 @@
5
5
  > presented as-is.
6
6
 
7
7
  The backend-agnostic half of Nimbus: a durable POSIX-like filesystem, a shell
8
- with 60+ Unix commands, and the WASI runtime layer with no Cloudflare
9
- dependency. You hand it a SQLite and get back `.fs` and `.exec`. On
10
- Cloudflare that SQLite is `ctx.storage.sql` inside your Durable Object; in bun
11
- or node it is `bun:sqlite` or `node:sqlite`.
8
+ with 60+ Unix commands, and the WASI runtime layer. It has no Cloudflare
9
+ dependency.
12
10
 
13
- I extracted this package because I kept wanting Nimbus *inside* other
14
- projects a Durable Object that already does something else but needs a real
15
- workspace, or a local script that needs the same filesystem semantics the
16
- hosted product has. The whole of it runs on two narrow ports (`SqlDatabase`
17
- and `SqlTransactions`), so the same code serves both hosts.
11
+ You hand it a SQLite and get back `.fs` and `.exec`. On Cloudflare that
12
+ SQLite is `ctx.storage.sql` inside your Durable Object; in bun or node it is
13
+ `bun:sqlite` or `node:sqlite`. The whole package rests on two narrow ports
14
+ (`SqlDatabase` and `SqlTransactions`), so the same code serves both hosts.
15
+
16
+ Use it when something you already run needs a real workspace. A Durable
17
+ Object that does something else and needs somewhere to work. A local script
18
+ that needs the filesystem semantics the hosted product has.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @nimbus-sh/core
24
+ ```
18
25
 
19
26
  ## Quick start
20
27
 
@@ -73,13 +80,13 @@ export class Workspace extends DurableObject {
73
80
  ```
74
81
 
75
82
  Files written through `.fs` are owned by the session user (uid 1000), not
76
- root, and the shell enforces the same permission model either way: a
77
- root-owned `/etc/passwd` refuses a write from `.fs`, and `id` resolves names
78
- through it.
83
+ root. The shell enforces the same permission model either way: a root-owned
84
+ `/etc/passwd` refuses a write from `.fs`, and `id` resolves names through
85
+ it.
79
86
 
80
87
  ## Real runtimes, off Cloudflare
81
88
 
82
- The wasm runtimes are separate npm packages so nobody downloads a Python
89
+ The wasm runtimes are separate npm packages, so nobody downloads a Python
83
90
  interpreter to get a filesystem. Install the ones you want and pass them in:
84
91
 
85
92
  ```bash
@@ -104,35 +111,49 @@ await ws.exec(`python -c "import sqlite3; print('live')"`); // CPython 3.13, re
104
111
  `@nimbus-sh/runtime-ruby` (Ruby 3.3) and `@nimbus-sh/runtime-clang` (clang →
105
112
  `wasm32-wasi`, compile and run C in the workspace) work the same way. Every
106
113
  package carries the same manifest and the same sha256-verified blobs the
107
- hosted product serves from R2 — one publisher, two transports.
114
+ hosted product serves from R2.
115
+
116
+ Without `facets` and `runtimes` you still get the full shell and coreutils.
117
+ The wasm runtimes are a dependency you add.
108
118
 
109
- Without `facets` and `runtimes` you still get the full shell and coreutils;
110
- the wasm runtimes are a dependency you add, not a mode you enable. One caveat:
111
- `localFacetHost()` covers bun and node only — on workerd the CSP forbids
119
+ `localFacetHost()` covers bun and node only. On workerd the CSP forbids
112
120
  request-time `WebAssembly.instantiate`, so wasm has to ride the Worker Loader
113
- module map, which is the machinery in `@nimbus-sh/worker` and
114
- `@nimbus-sh/fabric`; the shell, coreutils, and filesystem need none of it.
121
+ module map. That machinery lives in `@nimbus-sh/worker` and
122
+ `@nimbus-sh/fabric`. The shell, coreutils, and filesystem need none of it.
115
123
 
116
124
  ## Sharing a database with your own app
117
125
 
118
- The workspace is designed to be a tenant in a database you own, not the owner
119
- of it:
126
+ The workspace is a tenant in a database you own:
120
127
 
121
128
  - It creates and touches only its own tables (`inodes`, `file_chunks`,
122
129
  `content_lifecycle`, `vfs_*`).
123
- - `destroy()` drops exactly those tables. It never calls `deleteAll()`.
130
+ - `destroy()` drops those tables and does not call `deleteAll()`.
124
131
  - `transactionSync` must be a real transaction. An implementation that only
125
132
  calls the callback turns every atomic write into a torn one.
126
- - `generation` must never repeat across restarts of your host pids derive
133
+ - `generation` must never repeat across restarts of your host. Pids derive
127
134
  from it, and a repeated generation would hand a dead process live write
128
135
  authority.
129
136
 
130
- ## What needs the Worker package instead
137
+ When your own SQL rows must commit with filesystem bytes, use
138
+ `SqliteVFS.withTransaction(callback)` instead of an outer
139
+ `storage.transactionSync`. Use the credentialed synchronous VFS methods inside
140
+ the callback, not the workspace's asynchronous file methods. The callback can
141
+ read its writes; revisions and watch events publish only after commit. On
142
+ rollback, Nimbus clears cached chunks and reloads the always-resident inode
143
+ tree from SQLite before rethrowing with the original error as `cause`.
144
+
145
+ The method must own the outermost transaction on the same SQL host. Do not
146
+ nest it or start asynchronous work inside it. The host's transaction primitive
147
+ must support nested savepoints for individual VFS writes. If rollback reload
148
+ also fails, Nimbus throws an `AggregateError` carrying both failures; discard
149
+ that VFS instance and reopen it after storage recovers.
150
+
151
+ ## What the worker package adds
131
152
 
132
153
  Resident processes (long-running servers, attached TUIs), the session
133
154
  protocol, port routing to the public internet, and the hosted terminal all
134
- live in [`@nimbus-sh/worker`](https://www.npmjs.com/package/@nimbus-sh/worker),
135
- which composes on this package. If you want the full hosted product shape,
155
+ live in [`@nimbus-sh/worker`](https://www.npmjs.com/package/@nimbus-sh/worker).
156
+ That package composes on this one. If you want the full hosted product shape,
136
157
  start from `npx create-nimbus-app`.
137
158
 
138
159
  ## License
@@ -17,10 +17,11 @@
17
17
  /**
18
18
  * A command's output destination.
19
19
  *
20
- * `writeBytes` is present on sinks that store bytes verbatim (files). Sinks
21
- * that are inherently textual — the terminal, shell pipes omit it and
22
- * receive decoded text instead. This is a real capability difference, not a
23
- * fallback: a terminal has no way to hold a byte that is not text.
20
+ * `writeBytes` is present on sinks that store bytes verbatim files,
21
+ * `/dev/null`, and shell pipes, which carry the producer's exact bytes.
22
+ * Sinks without it take decoded text instead. This is a real capability
23
+ * difference, not a fallback: a terminal has no way to hold a byte that is
24
+ * not text.
24
25
  */
25
26
  export interface ByteSink {
26
27
  write(text: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"byte-stream.d.ts","sourceRoot":"","sources":["../../src/_shared/byte-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CACtC;AAED,2EAA2E;AAC3E,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,UAAU,CAAC;AAEzE,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAE5C;;;;GAIG;AACH,qBAAa,UAAU;IAIT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,OAAO,CAAK;gBAES,IAAI,EAAE,QAAQ;IAI3C,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAO9B,GAAG,IAAI,IAAI;CAKZ;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACvE,MAAM,CAkBR"}
1
+ {"version":3,"file":"byte-stream.d.ts","sourceRoot":"","sources":["../../src/_shared/byte-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CACtC;AAED,2EAA2E;AAC3E,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,UAAU,CAAC;AAEzE,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAE5C;;;;GAIG;AACH,qBAAa,UAAU;IAIT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,OAAO,CAAK;gBAES,IAAI,EAAE,QAAQ;IAI3C,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAO9B,GAAG,IAAI,IAAI;CAKZ;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACvE,MAAM,CAkBR"}
@@ -38,6 +38,13 @@
38
38
  * constructed Session's connect/post are inert.
39
39
  * Tools (e.g. nuxi) that open a Session purely
40
40
  * for optional profiling degrade cleanly.
41
+ * - node:zlib — full surface: every *Sync variant, brotli/zstd,
42
+ * crc32, constants, and streaming create* factories
43
+ * (probe-verified 2026-08-23 at compat date
44
+ * 2026-04-01). Forwarded verbatim by the zlib
45
+ * block in node-shims.ts; results are the host
46
+ * realm's own Buffers, which the widened
47
+ * __BufferMod.isBuffer recognizes.
41
48
  */
42
49
  export declare function getRealNodeImportsCode(): string;
43
50
  //# sourceMappingURL=real-node-imports.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"real-node-imports.d.ts","sourceRoot":"","sources":["../../src/_shared/real-node-imports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,wBAAgB,sBAAsB,IAAI,MAAM,CAU/C"}
1
+ {"version":3,"file":"real-node-imports.d.ts","sourceRoot":"","sources":["../../src/_shared/real-node-imports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,wBAAgB,sBAAsB,IAAI,MAAM,CAW/C"}
@@ -38,6 +38,13 @@
38
38
  * constructed Session's connect/post are inert.
39
39
  * Tools (e.g. nuxi) that open a Session purely
40
40
  * for optional profiling degrade cleanly.
41
+ * - node:zlib — full surface: every *Sync variant, brotli/zstd,
42
+ * crc32, constants, and streaming create* factories
43
+ * (probe-verified 2026-08-23 at compat date
44
+ * 2026-04-01). Forwarded verbatim by the zlib
45
+ * block in node-shims.ts; results are the host
46
+ * realm's own Buffers, which the widened
47
+ * __BufferMod.isBuffer recognizes.
41
48
  */
42
49
  export function getRealNodeImportsCode() {
43
50
  return `
@@ -48,5 +55,6 @@ import * as __real_diagnostics_channel from 'node:diagnostics_channel';
48
55
  import * as __real_repl from 'node:repl';
49
56
  import * as __real_vm from 'node:vm';
50
57
  import * as __real_inspector from 'node:inspector';
58
+ import * as __real_zlib from 'node:zlib';
51
59
  `.trim();
52
60
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * tarball.ts a whole tarball, in memory, as a path→bytes map.
2
+ * Tarball extraction for streaming installers and buffered archive consumers.
3
3
  *
4
4
  * The streaming primitives it walks with (`parseTarHeader`, `streamTarEntries`,
5
5
  * `readableStreamToAsyncIterable`) live in `./tarball-stream.ts` — a
@@ -7,11 +7,30 @@
7
7
  * into a string the loader pool injects into dynamic workers, where an import
8
8
  * would not resolve.
9
9
  *
10
- * This half is for a caller that already holds the bytes: `gem install`, which
11
- * fetches a `.gem` (a tar holding a gzipped tar) and needs both layers open at
12
- * once. An installer streaming a tarball it is still downloading should drive
13
- * `streamTarEntries` itself and never hold the whole thing.
10
+ * Installers use writeTarballStream. extractTarball retains a map for callers
11
+ * such as gem install, which must open an archive nested inside another one.
14
12
  */
13
+ export interface TarballWriteTarget {
14
+ exists(path: string): boolean;
15
+ mkdir(path: string, options?: {
16
+ recursive?: boolean;
17
+ }): void;
18
+ writeFile(path: string, data: Uint8Array | string): unknown;
19
+ }
20
+ export interface TarballWriteResult {
21
+ /** Regular files written, including the manifest. */
22
+ files: number;
23
+ /** Total decompressed bytes written. */
24
+ bytes: number;
25
+ }
26
+ /**
27
+ * Stream a gzipped npm archive into a package directory. Entry names are
28
+ * already canonical and prefix-stripped by streamTarEntries. Hold only the
29
+ * current entry and the manifest; write the manifest last so a failed install
30
+ * is not mistaken for a complete package on retry. A second manifest in one
31
+ * archive is a malformed package, not an overwrite. Filesystem failures reject.
32
+ */
33
+ export declare function writeTarballStream(body: ReadableStream<Uint8Array>, targetDir: string, vfs: TarballWriteTarget): Promise<TarballWriteResult>;
15
34
  /** Extract every regular file. Gzipped input is decompressed first. */
16
35
  export declare function extractTarball(tarball: ArrayBuffer): Promise<Map<string, Uint8Array>>;
17
36
  //# sourceMappingURL=tarball.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tarball.d.ts","sourceRoot":"","sources":["../../src/_shared/tarball.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,uEAAuE;AACvE,wBAAsB,cAAc,CAClC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAsBlC"}
1
+ {"version":3,"file":"tarball.d.ts","sourceRoot":"","sources":["../../src/_shared/tarball.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7D,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;CAC7D;AAED,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,kBAAkB,GACtB,OAAO,CAAC,kBAAkB,CAAC,CA6B7B;AAED,uEAAuE;AACvE,wBAAsB,cAAc,CAClC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAsBlC"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * tarball.ts a whole tarball, in memory, as a path→bytes map.
2
+ * Tarball extraction for streaming installers and buffered archive consumers.
3
3
  *
4
4
  * The streaming primitives it walks with (`parseTarHeader`, `streamTarEntries`,
5
5
  * `readableStreamToAsyncIterable`) live in `./tarball-stream.ts` — a
@@ -7,12 +7,49 @@
7
7
  * into a string the loader pool injects into dynamic workers, where an import
8
8
  * would not resolve.
9
9
  *
10
- * This half is for a caller that already holds the bytes: `gem install`, which
11
- * fetches a `.gem` (a tar holding a gzipped tar) and needs both layers open at
12
- * once. An installer streaming a tarball it is still downloading should drive
13
- * `streamTarEntries` itself and never hold the whole thing.
10
+ * Installers use writeTarballStream. extractTarball retains a map for callers
11
+ * such as gem install, which must open an archive nested inside another one.
14
12
  */
15
13
  import { streamTarEntries, readableStreamToAsyncIterable, } from './tarball-stream.js';
14
+ const PACKAGE_MANIFEST = 'package.json';
15
+ /**
16
+ * Stream a gzipped npm archive into a package directory. Entry names are
17
+ * already canonical and prefix-stripped by streamTarEntries. Hold only the
18
+ * current entry and the manifest; write the manifest last so a failed install
19
+ * is not mistaken for a complete package on retry. A second manifest in one
20
+ * archive is a malformed package, not an overwrite. Filesystem failures reject.
21
+ */
22
+ export async function writeTarballStream(body, targetDir, vfs) {
23
+ const ensureDir = (path) => {
24
+ if (!vfs.exists(path))
25
+ vfs.mkdir(path, { recursive: true });
26
+ };
27
+ ensureDir(targetDir);
28
+ let files = 0;
29
+ let bytes = 0;
30
+ let manifest = null;
31
+ const entries = streamTarEntries(readableStreamToAsyncIterable(body.pipeThrough(new DecompressionStream('gzip'))));
32
+ for await (const entry of entries) {
33
+ if (entry.name === PACKAGE_MANIFEST) {
34
+ if (manifest !== null) {
35
+ throw new Error(`tarball for ${targetDir} carried two ${PACKAGE_MANIFEST} entries`);
36
+ }
37
+ manifest = entry.data;
38
+ continue;
39
+ }
40
+ const fullPath = `${targetDir}/${entry.name}`;
41
+ const cut = fullPath.lastIndexOf('/');
42
+ if (cut > 0)
43
+ ensureDir(fullPath.slice(0, cut));
44
+ await vfs.writeFile(fullPath, entry.data);
45
+ files++;
46
+ bytes += entry.data.length;
47
+ }
48
+ if (!manifest)
49
+ throw new Error(`tarball for ${targetDir} carried no ${PACKAGE_MANIFEST}`);
50
+ await vfs.writeFile(`${targetDir}/${PACKAGE_MANIFEST}`, manifest);
51
+ return { files: files + 1, bytes: bytes + manifest.length };
52
+ }
16
53
  /** Extract every regular file. Gzipped input is decompressed first. */
17
54
  export async function extractTarball(tarball) {
18
55
  const files = new Map();
@@ -1 +1 @@
1
- {"version":3,"file":"vfs-write-ledger.d.ts","sourceRoot":"","sources":["../../src/_shared/vfs-write-ledger.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,QAqZpC,CAAC;AAET,eAAO,MAAM,uBAAuB,QAgC5B,CAAC"}
1
+ {"version":3,"file":"vfs-write-ledger.d.ts","sourceRoot":"","sources":["../../src/_shared/vfs-write-ledger.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,QA2ZpC,CAAC;AAET,eAAO,MAAM,uBAAuB,QAgC5B,CAAC"}
@@ -317,26 +317,32 @@ async function __nimbusPersistVfsWrite(supervisor, path, content, snapshot) {
317
317
  }
318
318
 
319
319
  /**
320
- * Write back everything parked, keeping any failure for the exit drain.
320
+ * Write back the cells parked at THIS instant, and only those.
321
321
  *
322
- * The write-back sites that are not the exit drain — the debounce below and
323
- * the RELEASE barrier ahead of egress have no caller who could act on a
324
- * failure: there is no user frame to throw into, and rejecting the fetch that
325
- * happened to trigger the flush would blame the wrong operation. Retaining
326
- * the failure in the channel \`__nimbusDrainVfsMutations\` already drains means
327
- * the exit path still reports it, so a lost write is loud exactly once and
328
- * never silent.
322
+ * Bounded on purpose, and deliberately not routed through
323
+ * \`__nimbusDrainVfsWrites\`, whose \`while (pending > 0)\` waits for the mutation
324
+ * queue to be EMPTY. That wait is correct at process exit, where no new writes
325
+ * are coming. Anywhere else it is a livelock: a facet unpacking a tarball adds
326
+ * mutations faster than the loop retires them, so the loop never returns.
327
+ * Sited ahead of egress where it was that stopped the request from ever
328
+ * leaving the facet, and \`npx sv create\` ran, printed its intro, and then
329
+ * never reported an exit at all. A barrier may delay a request; it may not
330
+ * wait on a condition a busy process never reaches.
331
+ *
332
+ * Failures are retained rather than thrown. The two callers — the debounce
333
+ * below, and the RELEASE barrier ahead of egress — have no frame that could
334
+ * act on one: rejecting the fetch that happened to trigger the flush would
335
+ * blame the wrong operation. The exit drain reports what is retained, so a
336
+ * lost write is loud exactly once and never silent.
329
337
  */
330
338
  async function __nimbusFlushVfsWriteBack(supervisor) {
331
339
  if (!supervisor) return;
332
- try {
333
- await __nimbusDrainVfsWrites(supervisor);
334
- } catch (error) {
335
- if (!__nimbusHasPendingVfsMutationFailure) {
336
- __nimbusHasPendingVfsMutationFailure = true;
337
- __nimbusPendingVfsMutationFailure = error;
338
- }
339
- }
340
+ const paths = Object.keys(__vfsWrites);
341
+ if (paths.length === 0) return;
342
+ await Promise.allSettled(paths.map((path) => __nimbusFlushVfsWrite(
343
+ path,
344
+ (content, snapshot) => __nimbusPersistVfsWrite(supervisor, path, content, snapshot),
345
+ )));
340
346
  }
341
347
 
342
348
  /**
@@ -1,37 +1,40 @@
1
1
  /**
2
- * Minimal byte-aligned comment stripper for prefetch's import detection.
2
+ * One JavaScript source scanner shared by the two comment-stripping
3
+ * call sites — prefetch's import detection and the esbuild transform
4
+ * pipeline's classifiers.
3
5
  *
4
- * Scope: strip `//` line comments and `/* … *\/` block comments by
5
- * replacing each comment with a single space. Newlines INSIDE the
6
- * comment are preserved so that line numbers stay aligned with the
7
- * input (matches the strip-and-classify pattern used by
8
- * `esbuild-service.ts:stripCommentsAndStrings` for the transform
9
- * pipeline).
6
+ * `scanJsSource(src, literals)` walks `src` once and returns a
7
+ * byte-aligned copy in which `//` and `/* *\/` comments are blanked
8
+ * (each comment becomes a space; newlines inside a block comment are
9
+ * preserved so line numbers still match the input). String, template
10
+ * and regex literals are what `literals` decides:
10
11
  *
11
- * Why a separate, smaller scanner: prefetch's IMPORT_RE / REQUIRE_RE
12
- * in `require-resolver.ts:41,87` are detection-only they extract
13
- * specifier strings, not shapes. The full string-and-template-literal
14
- * stripper in `esbuild-service.ts` is overkill for that, and crossing
15
- * the anti-touch boundary on `esbuild-service.ts` to extend a single
16
- * shared helper isn't worth it right now.
12
+ * - `'blank'`: literal content is replaced too templates keep their
13
+ * `${…}` interpolation expression as code so a depth-tracked caller
14
+ * sees `await` etc. inside it. This is the transform pipeline's
15
+ * classification view: nothing quoted can read as an `import`.
16
+ * - `'keep'`: literals are copied verbatim. This is prefetch's
17
+ * import-detection view: IMPORT_RE/REQUIRE_RE must see the
18
+ * specifier string, and a `//` or `/*` inside a literal must NOT
19
+ * open a comment that swallows a following real import.
17
20
  *
18
- * Empirical justification (esbuild-ast-rewrite wave, P2 measurement):
19
- * - Per-file AST-based extraction: ~5 ms warm avg
20
- * - 100-file session bootstrap with AST: ~553 ms (over the 500 ms gate)
21
- * - Regex with this stripper: ~0.1 ms per file
22
- * - Correctness gap closed: chalk `import { // eslint-disable\n a,\n b\n} from './utilities.js'` now matches
21
+ * The output is assembled from input slices and joined once. It used to
22
+ * be built one character at a time (`stripped += c`), which V8 keeps as
23
+ * a rope of one node per append — ~30 bytes of heap per character, all
24
+ * live until the string is first read. On typescript's 6.15 MB
25
+ * `lib/_tsc.js` that rope measured 174 MB against a 128 MB isolate
26
+ * the session Durable Object was killed inside `tsc`'s spawn before the
27
+ * facet existed. Spans hold the input, the output and a short array.
23
28
  *
24
- * String-literal handling is intentionally OUT of scope. A literal
25
- * `import x from 'y'` inside a JavaScript string would still produce
26
- * a false-positive prefetch attempt. The resolver no-ops on misses,
27
- * so it's a minor wasted-work cost — see IMPORT_RE header comment in
28
- * `require-resolver.ts:84-86`.
29
- *
30
- * TODO(CLN-X): when the `src/runtime/esbuild-service.ts` anti-touch
31
- * window opens, unify with the canonical
32
- * `stripCommentsAndStrings(src)` helper there. Both implement the
33
- * same logical pass over comments; only the string/regex-literal
34
- * handling differs.
29
+ * Serialized by name: `generateEsbuildTransformRuntimeSource` embeds
30
+ * this function's `.toString()` in the transform runtime, so every
31
+ * constant it reads is declared inside the body.
32
+ */
33
+ export declare function scanJsSource(src: string, literals: 'keep' | 'blank'): string;
34
+ /**
35
+ * Strip comments for import/require detection. Literals are kept: the
36
+ * specifier the regexes extract lives inside a string, and a comment
37
+ * marker inside a string must not swallow the code that follows it.
35
38
  */
36
39
  export declare function stripCommentsForImports(src: string): string;
37
40
  //# sourceMappingURL=comment-strip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"comment-strip.d.ts","sourceRoot":"","sources":["../../src/runtime/comment-strip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqC3D"}
1
+ {"version":3,"file":"comment-strip.d.ts","sourceRoot":"","sources":["../../src/runtime/comment-strip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAqM5E;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3D"}