@nimbus-sh/worker 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 (184) hide show
  1. package/README.md +2 -2
  2. package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
  3. package/dist/cirrus-npm-cjs.generated.js +1 -1
  4. package/dist/facets/cirrus-real.d.ts.map +1 -1
  5. package/dist/facets/cirrus-real.js +6 -5
  6. package/dist/facets/compose.d.ts +148 -0
  7. package/dist/facets/compose.d.ts.map +1 -0
  8. package/dist/facets/compose.js +146 -0
  9. package/dist/facets/durable-images.d.ts +9 -5
  10. package/dist/facets/durable-images.d.ts.map +1 -1
  11. package/dist/facets/durable-images.js +17 -7
  12. package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
  13. package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
  14. package/dist/facets/esbuild-bundle-pool.js +122 -0
  15. package/dist/facets/exec-telemetry.d.ts +8 -0
  16. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  17. package/dist/facets/manager.d.ts +240 -109
  18. package/dist/facets/manager.d.ts.map +1 -1
  19. package/dist/facets/manager.js +677 -342
  20. package/dist/facets/process.d.ts +20 -16
  21. package/dist/facets/process.d.ts.map +1 -1
  22. package/dist/facets/process.js +59 -28
  23. package/dist/facets/real-vite-fs-shim.js +2 -2
  24. package/dist/facets/real-vite-hmr.d.ts +3 -1
  25. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  26. package/dist/facets/real-vite-hmr.js +18 -8
  27. package/dist/facets/vite-dev-server.d.ts +25 -47
  28. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  29. package/dist/facets/vite-dev-server.js +185 -219
  30. package/dist/facets/wasm-image-digest.d.ts +25 -0
  31. package/dist/facets/wasm-image-digest.d.ts.map +1 -0
  32. package/dist/facets/wasm-image-digest.js +25 -0
  33. package/dist/facets/wasm-swap-registry.d.ts +51 -92
  34. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  35. package/dist/facets/wasm-swap-registry.js +68 -227
  36. package/dist/facets/ws-terminal.d.ts +9 -31
  37. package/dist/facets/ws-terminal.d.ts.map +1 -1
  38. package/dist/facets/ws-terminal.js +58 -51
  39. package/dist/git/network-facet.d.ts.map +1 -1
  40. package/dist/git/network-facet.js +6 -5
  41. package/dist/hosted/commands.d.ts +12 -0
  42. package/dist/hosted/commands.d.ts.map +1 -0
  43. package/dist/hosted/commands.js +1658 -0
  44. package/dist/hosted/runtime.d.ts +114 -0
  45. package/dist/hosted/runtime.d.ts.map +1 -0
  46. package/dist/hosted/runtime.js +288 -0
  47. package/dist/hosted/services.d.ts +40 -0
  48. package/dist/hosted/services.d.ts.map +1 -0
  49. package/dist/hosted/services.js +512 -0
  50. package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
  51. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  52. package/dist/loaders/child-process/spawn-pool.js +3 -2
  53. package/dist/loaders/generated-workers.d.ts +2 -2
  54. package/dist/loaders/generated-workers.d.ts.map +1 -1
  55. package/dist/loaders/generated-workers.js +3 -3
  56. package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
  57. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  58. package/dist/loaders/npm-resolve-preamble.js +34 -108
  59. package/dist/node-shims-artifact.generated.js +3 -3
  60. package/dist/npm/install-batch-facet.d.ts +3 -2
  61. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  62. package/dist/npm/install-batch-facet.js +45 -15
  63. package/dist/npm/installer.d.ts +22 -17
  64. package/dist/npm/installer.d.ts.map +1 -1
  65. package/dist/npm/installer.js +209 -193
  66. package/dist/npm/pre-bundle-facet.d.ts +1 -1
  67. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  68. package/dist/npm/pre-bundle-facet.js +1 -1
  69. package/dist/npm/resolve-facet.d.ts +6 -0
  70. package/dist/npm/resolve-facet.d.ts.map +1 -1
  71. package/dist/npm/resolve-one-facet.d.ts +21 -20
  72. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  73. package/dist/npm/resolve-one-facet.js +85 -84
  74. package/dist/npm/semver.d.ts +59 -0
  75. package/dist/npm/semver.d.ts.map +1 -0
  76. package/dist/npm/semver.js +226 -0
  77. package/dist/router/remote-api.js +31 -1
  78. package/dist/runtime/bash-repl.d.ts +1 -1
  79. package/dist/runtime/bash-repl.d.ts.map +1 -1
  80. package/dist/runtime/bash-repl.js +38 -4
  81. package/dist/runtime/bun-runner.js +1 -1
  82. package/dist/runtime/html-entrypoint.d.ts +7 -0
  83. package/dist/runtime/html-entrypoint.d.ts.map +1 -1
  84. package/dist/runtime/html-entrypoint.js +18 -2
  85. package/dist/runtime/node-runner.d.ts +4 -1
  86. package/dist/runtime/node-runner.d.ts.map +1 -1
  87. package/dist/runtime/node-runner.js +9 -2
  88. package/dist/runtime/node-shims.d.ts.map +1 -1
  89. package/dist/runtime/node-shims.js +305 -29
  90. package/dist/runtime/opencode-facet-runner.js +16 -16
  91. package/dist/runtime/package-manager.d.ts +35 -122
  92. package/dist/runtime/package-manager.d.ts.map +1 -1
  93. package/dist/runtime/package-manager.js +61 -596
  94. package/dist/runtime/process-logs-api.d.ts +1 -1
  95. package/dist/runtime/process-logs-api.d.ts.map +1 -1
  96. package/dist/runtime/process-logs-api.js +1 -1
  97. package/dist/runtime/python-repl.d.ts +2 -1
  98. package/dist/runtime/python-repl.d.ts.map +1 -1
  99. package/dist/runtime/python-repl.js +82 -59
  100. package/dist/runtime/repl-session.d.ts +9 -47
  101. package/dist/runtime/repl-session.d.ts.map +1 -1
  102. package/dist/runtime/repl-session.js +105 -105
  103. package/dist/runtime/runtime-catalog.d.ts +13 -0
  104. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  105. package/dist/runtime/runtime-catalog.js +88 -0
  106. package/dist/session/agent.d.ts +1 -21
  107. package/dist/session/agent.d.ts.map +1 -1
  108. package/dist/session/ai.d.ts +5 -1
  109. package/dist/session/ai.d.ts.map +1 -1
  110. package/dist/session/ai.js +7 -0
  111. package/dist/session/helpers.d.ts +38 -0
  112. package/dist/session/helpers.d.ts.map +1 -1
  113. package/dist/session/helpers.js +76 -0
  114. package/dist/session/hibernation.d.ts +2 -1
  115. package/dist/session/hibernation.d.ts.map +1 -1
  116. package/dist/session/hibernation.js +7 -4
  117. package/dist/session/init-phases.d.ts +1 -1
  118. package/dist/session/init-phases.d.ts.map +1 -1
  119. package/dist/session/init-phases.js +2 -12
  120. package/dist/session/init.d.ts +15 -36
  121. package/dist/session/init.d.ts.map +1 -1
  122. package/dist/session/init.js +50 -2327
  123. package/dist/session/loopback.d.ts +2 -29
  124. package/dist/session/loopback.d.ts.map +1 -1
  125. package/dist/session/loopback.js +3 -27
  126. package/dist/session/nimbus-session.d.ts +77 -34
  127. package/dist/session/nimbus-session.d.ts.map +1 -1
  128. package/dist/session/nimbus-session.js +126 -514
  129. package/dist/session/npm-install-port.d.ts +18 -0
  130. package/dist/session/npm-install-port.d.ts.map +1 -0
  131. package/dist/session/npm-install-port.js +37 -0
  132. package/dist/session/port-capability.d.ts +44 -0
  133. package/dist/session/port-capability.d.ts.map +1 -1
  134. package/dist/session/port-capability.js +110 -0
  135. package/dist/session/programmatic.d.ts +36 -43
  136. package/dist/session/programmatic.d.ts.map +1 -1
  137. package/dist/session/programmatic.js +109 -115
  138. package/dist/session/routes.d.ts +30 -17
  139. package/dist/session/routes.d.ts.map +1 -1
  140. package/dist/session/routes.js +42 -131
  141. package/dist/session/rpc.d.ts +15 -15
  142. package/dist/session/rpc.d.ts.map +1 -1
  143. package/dist/session/rpc.js +98 -49
  144. package/dist/session/serving-port.d.ts +27 -0
  145. package/dist/session/serving-port.d.ts.map +1 -0
  146. package/dist/session/serving-port.js +27 -0
  147. package/dist/session/start-real-vite.d.ts +12 -0
  148. package/dist/session/start-real-vite.d.ts.map +1 -1
  149. package/dist/session/start-real-vite.js +4 -4
  150. package/dist/session/supervisor-op.d.ts +4 -4
  151. package/dist/session/supervisor-op.d.ts.map +1 -1
  152. package/dist/session/supervisor-op.js +24 -10
  153. package/dist/session/supervisor-rpc.d.ts +7 -7
  154. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  155. package/dist/session/supervisor-rpc.js +4 -0
  156. package/dist/session/vite-command.d.ts +18 -0
  157. package/dist/session/vite-command.d.ts.map +1 -0
  158. package/dist/session/vite-command.js +609 -0
  159. package/dist/session/ws.d.ts +61 -0
  160. package/dist/session/ws.d.ts.map +1 -1
  161. package/dist/session/ws.js +122 -12
  162. package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
  163. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  164. package/dist/shell/npm-bin-entrypoints.js +12 -85
  165. package/dist/workspace-host.d.ts +6 -0
  166. package/dist/workspace-host.d.ts.map +1 -0
  167. package/dist/workspace-host.js +5 -0
  168. package/dist/workspace.d.ts +2 -0
  169. package/dist/workspace.d.ts.map +1 -0
  170. package/dist/workspace.js +1 -0
  171. package/package.json +34 -9
  172. package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
  173. package/public/index.html +1 -1
  174. package/scripts/bundle-facet-workers.mjs +1 -1
  175. package/scripts/bundle-npm-cjs.mjs +2 -2
  176. package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
  177. package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
  178. package/dist/facets/on-demand-bundle-gate.js +0 -125
  179. package/dist/npm/install-args.d.ts +0 -21
  180. package/dist/npm/install-args.d.ts.map +0 -1
  181. package/dist/npm/install-args.js +0 -70
  182. package/dist/npm/npm-log.d.ts +0 -31
  183. package/dist/npm/npm-log.d.ts.map +0 -1
  184. package/dist/npm/npm-log.js +0 -51
@@ -0,0 +1,512 @@
1
+ import { staticStdinReader } from "@nimbus-sh/core/shell/stdin-adapter.js";
2
+ import { composeFacetManager } from "../facets/compose.js";
3
+ import { FacetProcessManager, textBytes } from "../facets/process.js";
4
+ import { ChildProcessSpawnPool } from "../loaders/child-process/spawn-pool.js";
5
+ import { CRED_KERNEL, CRED_SESSION_USER } from "@nimbus-sh/core/runtime/os-contracts.js";
6
+ import { EsbuildBundlePool } from "../facets/esbuild-bundle-pool.js";
7
+ import { EsbuildService } from "@nimbus-sh/core/runtime/esbuild-service.js";
8
+ import { CF_COMPAT_DATE } from "@nimbus-sh/core/constants.js";
9
+ import { notifyTerminalEvent } from "../runtime/process-logs-api.js";
10
+ // The supervisor terminates a facet's outbound sockets so inbound frames
11
+ // arrive as supervisor replies (VFS coherence witness 3).
12
+ import { WebSocketRelay } from "../session/ws-relay.js";
13
+ // ── Pure helpers extracted to ./nimbus-session-helpers.ts (S1) ────────
14
+ //
15
+ // renderNoDevServerHtml, BUNDLER_BIN_PREFIXES, NIMBUS_UNSUPPORTED_BINS,
16
+ // WRANGLER_IGNORED_FLAGS{,_WITH_VALUE}, WRANGLER_UNSUPPORTED_CONFIG_FIELDS,
17
+ // filterWranglerFlags, detectUnsupportedWranglerConfig, _CP_FACET_DIRECT,
18
+ // _CP_PURE_BUILTIN, _classifyCommand, detectBundlerBin, checkNodeModulesGuard
19
+ // all live in the helpers module now.
20
+ //
21
+ // They are imported here (so call sites in this file work unchanged) and
22
+ // re-exported (so external callers importing them from
23
+ // `./nimbus-session.js` keep working — back-compat).
24
+ //
25
+ // (esbuild wasm bytes are fetched from env.ASSETS by
26
+ // src/esbuild-wasm-bytes.ts at pool-construction time; A'.5 dropped
27
+ // the supervisor-resident cache + the SUPERVISOR.getEsbuildWasm RPC.)
28
+ // Helpers needed by this class file's own logic (not just re-export).
29
+ import { _classifyCommand } from "../session/helpers.js";
30
+ import { z } from "zod/v4";
31
+ const CpFacetDirectPayloadSchema = z.object({
32
+ command: z.unknown().optional().transform((value) => value == null ? '' : String(value)),
33
+ args: z.array(z.unknown()).optional().transform((value) => (value || []).map((item) => String(item))),
34
+ env: z.record(z.string(), z.unknown()).optional().transform((value) => {
35
+ const out = {};
36
+ for (const [key, item] of Object.entries(value || {}))
37
+ out[key] = String(item);
38
+ return out;
39
+ }),
40
+ cwd: z.unknown().optional().transform((value) => value == null ? '/' : String(value)),
41
+ stdin: z.unknown().optional().transform((value) => value == null ? '' : String(value)),
42
+ processPid: z.number().int().positive(),
43
+ }).passthrough();
44
+ function normalizeCpCommandName(name) {
45
+ const text = String(name || '').trim();
46
+ if (!text.startsWith('/'))
47
+ return text;
48
+ const slash = text.lastIndexOf('/');
49
+ const base = slash >= 0 ? text.slice(slash + 1) : text;
50
+ const dir = text.slice(0, Math.max(0, text.length - base.length));
51
+ if (dir === '/bin/' || dir === '/usr/bin/' || dir === '/usr/local/bin/') {
52
+ return base;
53
+ }
54
+ return text;
55
+ }
56
+ export function ensureBundlePool(self, runtimeContext) {
57
+ if (!self.bundlePool)
58
+ self.bundlePool = new EsbuildBundlePool(runtimeContext.env, runtimeContext.ctx);
59
+ return self.bundlePool;
60
+ }
61
+ export function ensureFacetManager(self, runtimeContext) {
62
+ if (!self.facetManagerComposed) {
63
+ // The manager is composed over the filesystem, so the filesystem comes
64
+ // first. Cheap and idempotent; every caller already stood it up or is
65
+ // about to.
66
+ self.ensureSqliteFs();
67
+ self.facetManagerComposed = composeFacetManager({
68
+ ctx: runtimeContext.ctx,
69
+ env: runtimeContext.env,
70
+ processes: self.processes,
71
+ portRegistry: self.portRegistry,
72
+ vfs: self.sqliteFs,
73
+ ...(self.esbuildService ? { esbuild: self.esbuildService } : {}),
74
+ hooks: {
75
+ onExternalExit: (pid, code, reason) => self._reportExternalExit(pid, code, reason),
76
+ requestLaunchTurn: (notBefore) => { void runtimeContext.requestLaunchTurn(notBefore); },
77
+ notify: (line) => runtimeContext.notify(line),
78
+ onSpawn: (pid, command, longRunning) => {
79
+ const attachedTty = self.processes.get(pid)?.attachedTty === true;
80
+ if (longRunning) {
81
+ try {
82
+ self.processes.openInput(pid);
83
+ }
84
+ catch { }
85
+ }
86
+ // Only surface long-running / user-visible spawns to keep
87
+ // the terminal uncluttered. Short `node <file>` evals also
88
+ // get a line because users want the pid for `logs`/`kill`.
89
+ if (!self.terminal)
90
+ return;
91
+ const label = longRunning ? 'started (long-running)' : 'started';
92
+ self.terminal.write(`\x1b[2m[facet ${label}: pid=${pid} cmd="${command}"]\x1b[0m\r\n`);
93
+ // Structured event so the tabs UI can auto-open a log tab
94
+ // for long-running processes (vite, wrangler dev, etc.).
95
+ notifyTerminalEvent(self.terminal, {
96
+ type: 'spawn', pid, command, longRunning, attachedTty,
97
+ });
98
+ },
99
+ },
100
+ });
101
+ }
102
+ const composed = self.facetManagerComposed;
103
+ // W3.5 Fix B: share the session's lazy esbuildService with the
104
+ // FacetManager so the bundle's ESM→CJS pre-pass doesn't pay
105
+ // wasm-init twice. The session may construct it after the manager
106
+ // exists, so the share is re-offered on every call; FacetManager
107
+ // otherwise lazy-creates its own on first exec — same wasm bytes,
108
+ // same ~10ms init cost, just paid once per surface.
109
+ if (self.esbuildService) {
110
+ composed.manager.setEsbuildService(self.esbuildService);
111
+ }
112
+ return composed;
113
+ }
114
+ export function _ensureWebSocketRelay(self, runtimeContext) {
115
+ if (!self.webSocketRelay)
116
+ self.webSocketRelay = new WebSocketRelay();
117
+ return self.webSocketRelay;
118
+ }
119
+ export function _ensureFacetProcessManager(self, runtimeContext) {
120
+ if (self.facetProcessManager)
121
+ return self.facetProcessManager;
122
+ self.ensureSqliteFs();
123
+ self.ensureFacetManager();
124
+ // FacetProcessManager is statically imported at top-of-file (W8).
125
+ // No lazy-import: workerd doesn't ship CJS require, and the dynamic
126
+ // import would be async — making _ensureFacetProcessManager async
127
+ // would force every cp* RPC entry point to also be async on the
128
+ // promise-resolution path, which is fine but uglier. Compile-time
129
+ // tree-shaking handles unused-when-no-cp-RPC paths.
130
+ // Adapter for FacetManagerLike — wraps the existing FacetManager.exec
131
+ // with a streaming surface. Phase 1 simplification: facet-direct
132
+ // commands are dispatched through the shell registry the same way
133
+ // shell.execute does, but with the per-PID hooks routed.
134
+ const facetMgrAdapter = {
135
+ execStream: async (codeJson, opts, hooks) => {
136
+ // codeJson is a payload from FacetProcessManager._dispatch facet-direct
137
+ // path: {command, args, env, cwd, stdin}. We dispatch through the
138
+ // existing shell registry by resolving the command and invoking
139
+ // it with synthesized output streams that route to hooks.
140
+ let payload;
141
+ try {
142
+ const parsed = CpFacetDirectPayloadSchema.safeParse(JSON.parse(codeJson));
143
+ if (!parsed.success) {
144
+ hooks.onStderr(textBytes('child_process: facet dispatch requires a broker-assigned process pid\n'));
145
+ return 1;
146
+ }
147
+ payload = parsed.data;
148
+ }
149
+ catch {
150
+ hooks.onStderr(textBytes('child_process: invalid facet dispatch payload\n'));
151
+ return 1;
152
+ }
153
+ const registry = self._cpRegistry;
154
+ if (!registry) {
155
+ hooks.onStderr(textBytes('child_process: command registry unavailable\n'));
156
+ return 127;
157
+ }
158
+ const commandName = normalizeCpCommandName(payload.command);
159
+ const cmd = await registry.resolve(commandName);
160
+ if (!cmd) {
161
+ hooks.onStderr(textBytes(`${payload.command}: command not found\n`));
162
+ return 127;
163
+ }
164
+ // Synthesize a CommandContext for the internal shell substrate.
165
+ const stdoutStream = { write: (d) => hooks.onStdout(textBytes(String(d))) };
166
+ const stderrStream = { write: (d) => hooks.onStderr(textBytes(String(d))) };
167
+ const ac = new AbortController();
168
+ const cred = self.processes.cred(payload.processPid);
169
+ const ctx = {
170
+ pid: payload.processPid,
171
+ cred,
172
+ args: payload.args || [],
173
+ env: payload.env || {},
174
+ cwd: payload.cwd || '/home/user',
175
+ vfs: self.sqliteFs.as(cred),
176
+ stdout: stdoutStream,
177
+ stderr: stderrStream,
178
+ signal: ac.signal,
179
+ // For commands that need stdin we pass a tiny adapter.
180
+ stdin: staticStdinReader(payload.stdin || ''),
181
+ setUmask: (mask) => { self.processes.setUmask(payload.processPid, mask); },
182
+ runAs: async (targetCred, argv) => {
183
+ if (argv.length === 0)
184
+ return 0;
185
+ const child = self.processes.spawn(argv.join(' '), argv, payload.cwd, { parentPid: payload.processPid, cred: targetCred });
186
+ let exitCode = 1;
187
+ try {
188
+ exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), payload.env, payload.cwd, payload.stdin, hooks);
189
+ return exitCode;
190
+ }
191
+ finally {
192
+ self.processes.exit(child.pid, exitCode);
193
+ }
194
+ },
195
+ __nimbusCaptureOutput: true,
196
+ };
197
+ try {
198
+ const code = await cmd(ctx);
199
+ return typeof code === 'number' ? code : 0;
200
+ }
201
+ catch (e) {
202
+ hooks.onStderr(textBytes(`${payload.command}: ${e?.message || String(e)}\n`));
203
+ return 1;
204
+ }
205
+ },
206
+ abort: (facetName) => {
207
+ // Best-effort: relay to ctx.facets.abort, mirroring FacetManager.kill.
208
+ try {
209
+ runtimeContext.ctx.facets?.abort?.(facetName, new Error('SIGKILL'));
210
+ }
211
+ catch { }
212
+ return true;
213
+ },
214
+ };
215
+ // Adapter for CommandRegistryLike. The shared shell registry is
216
+ // attached to `this._cpRegistry` by the shell-init path (see
217
+ // construction near line 2058 — registry passed as ctor arg there).
218
+ const cmdRegistryAdapter = {
219
+ // Consult the live shell registry FIRST so dynamically-registered
220
+ // commands (registerUnixCommands / git / npm / wrangler etc.) are seen
221
+ // even if they're not in the static _CP_PURE_BUILTIN allow-list. Falls
222
+ // back to the static facet-direct table for known facet-only commands.
223
+ // Returns null
224
+ // (→ exit 127) for everything unknown.
225
+ resolve: (name) => {
226
+ const commandName = normalizeCpCommandName(name);
227
+ const registry = self._cpRegistry;
228
+ if (registry && typeof registry.has === 'function' && registry.has(commandName)) {
229
+ // Registered — classify by name. Reuse the static table so
230
+ // facet-direct commands (node/npm/git/...) keep their kind
231
+ // even when they ALSO happen to be registry entries.
232
+ return _classifyCommand(commandName) || { kind: 'pure-builtin' };
233
+ }
234
+ return _classifyCommand(commandName);
235
+ },
236
+ runPureBuiltin: async (pid, name, args, env, cwd, stdin, hooks) => {
237
+ const registry = self._cpRegistry;
238
+ if (!registry) {
239
+ hooks.onStderr(textBytes('cp: registry unavailable\n'));
240
+ return 127;
241
+ }
242
+ const commandName = normalizeCpCommandName(name);
243
+ const cmd = await registry.resolve(commandName);
244
+ if (!cmd) {
245
+ hooks.onStderr(textBytes(`${name}: command not found\n`));
246
+ return 127;
247
+ }
248
+ const cred = self.processes.cred(pid);
249
+ const ac = new AbortController();
250
+ const ctx = {
251
+ pid,
252
+ cred,
253
+ args, env, cwd,
254
+ vfs: self.sqliteFs.as(cred),
255
+ stdout: { write: (d) => hooks.onStdout(textBytes(String(d))) },
256
+ stderr: { write: (d) => hooks.onStderr(textBytes(String(d))) },
257
+ signal: ac.signal,
258
+ stdin: staticStdinReader(stdin),
259
+ setUmask: (mask) => { self.processes.setUmask(pid, mask); },
260
+ runAs: async (targetCred, argv) => {
261
+ if (argv.length === 0)
262
+ return 0;
263
+ const child = self.processes.spawn(argv.join(' '), argv, cwd, { parentPid: pid, cred: targetCred });
264
+ let exitCode = 1;
265
+ try {
266
+ exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), env, cwd, stdin, hooks);
267
+ return exitCode;
268
+ }
269
+ finally {
270
+ self.processes.exit(child.pid, exitCode);
271
+ }
272
+ },
273
+ };
274
+ try {
275
+ const code = await cmd(ctx);
276
+ return typeof code === 'number' ? code : 0;
277
+ }
278
+ catch (e) {
279
+ hooks.onStderr(textBytes(`${name}: ${e?.message || String(e)}\n`));
280
+ return 1;
281
+ }
282
+ },
283
+ };
284
+ // Construct the child-process Loader pool when the binding is available.
285
+ // Unit-test hosts without LOADER continue through direct dispatch.
286
+ let spawnPool;
287
+ try {
288
+ const envAny = runtimeContext.env;
289
+ if (envAny?.LOADER && typeof envAny.LOADER.get === 'function') {
290
+ spawnPool = new ChildProcessSpawnPool(runtimeContext.env, runtimeContext.ctx);
291
+ }
292
+ }
293
+ catch {
294
+ spawnPool = undefined;
295
+ }
296
+ self.facetProcessManager = new FacetProcessManager({
297
+ facetMgr: facetMgrAdapter,
298
+ processes: self.processes,
299
+ vfsForProcess: (pid) => self.sqliteFs.as(self.processes.cred(pid)),
300
+ commandRegistry: cmdRegistryAdapter,
301
+ shellExecutor: {
302
+ execute: async (pid, commandLine, env, cwd, stdin, hooks) => {
303
+ if (!self.shell) {
304
+ hooks.onStderr(textBytes('sh: shell unavailable\n'));
305
+ return 127;
306
+ }
307
+ const cred = self.processes.cred(pid);
308
+ const setUmask = (mask) => { self.processes.setUmask(pid, mask); };
309
+ const runAs = async (_parent, targetCred, argv) => {
310
+ if (argv.length === 0)
311
+ return 0;
312
+ const child = self.processes.spawn(argv.join(' '), argv, cwd, { parentPid: pid, cred: targetCred });
313
+ let exitCode = 1;
314
+ try {
315
+ exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), env, cwd, stdin, hooks);
316
+ return exitCode;
317
+ }
318
+ finally {
319
+ self.processes.exit(child.pid, exitCode);
320
+ }
321
+ };
322
+ const result = await self.shell.execute(String(commandLine), {
323
+ cwd: cwd || '/home/user',
324
+ env: { ...self.shell.env, ...(env || {}) },
325
+ onStdout: hooks.onStdout,
326
+ onStderr: hooks.onStderr,
327
+ stdin,
328
+ isolateShellState: true,
329
+ commandContext: { pid, cred, setUmask },
330
+ runAs,
331
+ });
332
+ return typeof result?.exitCode === 'number' ? result.exitCode : 0;
333
+ },
334
+ },
335
+ ctx: runtimeContext.ctx,
336
+ spawnPool,
337
+ });
338
+ return self.facetProcessManager;
339
+ }
340
+ export function ensureFetchProxy(self, runtimeContext, log) {
341
+ if (self.fetchProxyEntrypoint)
342
+ return self.fetchProxyEntrypoint;
343
+ try {
344
+ const env = runtimeContext.env;
345
+ if (!env?.LOADER?.load) {
346
+ log?.('LOADER.load not available — using global fetch');
347
+ return null;
348
+ }
349
+ // Buffered proxy: reads the entire response body into an ArrayBuffer
350
+ // and returns it in ONE message instead of forwarding a ReadableStream.
351
+ // In workerd local dev, streaming responses across a service-binding
352
+ // RPC fabric opens a separate loopback socket PER chunk (~16KB), which
353
+ // exhausts ephemeral ports for larger installs (npm registry packuments
354
+ // are 500KB-3MB, tarballs up to 5MB). Buffering to arrayBuffer means
355
+ // 1 stub call = 1 loopback connection, not N connections.
356
+ //
357
+ // 32MB cap prevents a malformed giant response from OOMing the proxy
358
+ // isolate. Packages with tarballs larger than 32MB will fail to install
359
+ // cleanly (returned as 413 → caller treats as failed fetch).
360
+ const proxyCode = [
361
+ 'const MAX_BYTES = 32 * 1024 * 1024;',
362
+ 'export default {',
363
+ ' async fetch(request, workerEnv) {',
364
+ ' try {',
365
+ ' const body = await request.json();',
366
+ ' const resp = await fetch(body.url, {',
367
+ ' method: body.method || "GET",',
368
+ ' headers: body.headers || {},',
369
+ ' });',
370
+ ' // Check advertised Content-Length before buffering',
371
+ ' const clStr = resp.headers.get("content-length");',
372
+ ' if (clStr) {',
373
+ ' const cl = parseInt(clStr, 10);',
374
+ ' if (cl > MAX_BYTES) {',
375
+ ' return new Response(',
376
+ ' JSON.stringify({ error: "response too large: " + cl + " bytes (cap " + MAX_BYTES + ")" }),',
377
+ ' { status: 413, headers: { "Content-Type": "application/json" } }',
378
+ ' );',
379
+ ' }',
380
+ ' }',
381
+ ' // Buffer entire body — ONE message, not streamed chunks',
382
+ ' const buf = await resp.arrayBuffer();',
383
+ ' if (buf.byteLength > MAX_BYTES) {',
384
+ ' return new Response(',
385
+ ' JSON.stringify({ error: "response exceeded cap: " + buf.byteLength + " bytes" }),',
386
+ ' { status: 413, headers: { "Content-Type": "application/json" } }',
387
+ ' );',
388
+ ' }',
389
+ ' return new Response(buf, {',
390
+ ' status: resp.status,',
391
+ ' statusText: resp.statusText,',
392
+ ' headers: Object.fromEntries(resp.headers.entries()),',
393
+ ' });',
394
+ ' } catch (e) {',
395
+ ' return new Response(JSON.stringify({ error: e.message }), {',
396
+ ' status: 502,',
397
+ ' headers: { "Content-Type": "application/json" },',
398
+ ' });',
399
+ ' }',
400
+ ' }',
401
+ '};',
402
+ ].join('\n');
403
+ const worker = env.LOADER.load({
404
+ compatibilityDate: CF_COMPAT_DATE,
405
+ compatibilityFlags: ['nodejs_compat'],
406
+ mainModule: 'fetch-proxy.js',
407
+ modules: { 'fetch-proxy.js': proxyCode },
408
+ });
409
+ self.fetchProxyEntrypoint = worker.getEntrypoint();
410
+ log?.('Fetch proxy worker created (singleton)');
411
+ return self.fetchProxyEntrypoint;
412
+ }
413
+ catch (e) {
414
+ log?.(`Fetch proxy creation failed: ${e?.message}`);
415
+ return null;
416
+ }
417
+ }
418
+ export function buildFetchFn(self, runtimeContext, log) {
419
+ const entrypoint = self.ensureFetchProxy(log);
420
+ if (!entrypoint)
421
+ return undefined;
422
+ return async (url, init) => {
423
+ const headers = {};
424
+ if (init?.headers) {
425
+ if (init.headers instanceof Headers) {
426
+ init.headers.forEach((v, k) => { headers[k] = v; });
427
+ }
428
+ else if (typeof init.headers === 'object') {
429
+ Object.assign(headers, init.headers);
430
+ }
431
+ }
432
+ return entrypoint.fetch(new Request('http://fetch-proxy/do-fetch', {
433
+ method: 'POST',
434
+ body: JSON.stringify({ url, method: init?.method || 'GET', headers }),
435
+ }));
436
+ };
437
+ }
438
+ export async function ensureNpmInstaller(self, runtimeContext, onProgress) {
439
+ self.ensureSqliteFs();
440
+ if (!self.esbuildService) {
441
+ if (!self.sqliteFs)
442
+ throw new Error('Session VFS is not initialized');
443
+ self.esbuildService = new EsbuildService(self.sqliteFs.as(CRED_KERNEL));
444
+ }
445
+ // Lazy-load the installer (+ its ~216 KB resolver/facet/loader-pool
446
+ // subgraph) on first npm use so it stays out of the cold script-eval
447
+ // graph. The install command paths that call this are already async.
448
+ const { NpmInstaller } = await import('../npm/installer.js');
449
+ // ── Lazy fetch-proxy ────────────────────────────────────────────
450
+ // The fetch-proxy is a singleton dynamic worker (LOADER.load) that
451
+ // buffers registry responses to dodge wrangler-local-dev port
452
+ // exhaustion. It is only needed for the in-supervisor npm paths.
453
+ // When the resolver and install paths run in facets (default-on),
454
+ // they use bare globalThis.fetch and need no proxy.
455
+ //
456
+ // workerd has a per-DO cap on concurrent dynamic workers (~5-6
457
+ // empirically). A permanent live proxy worker eats one of those
458
+ // slots for the entire DO lifetime, so the proxy is built only when
459
+ // any facet path is disabled via its env flag.
460
+ const useFacetResolver = self._envFlagDefaultOn('NIMBUS_FACET_RESOLVER');
461
+ const useFacetInstall = self._envFlagDefaultOn('NIMBUS_FACET_NPM_INSTALL');
462
+ const useBatchFacet = self._envFlagDefaultOn('NIMBUS_FACET_NPM_INSTALL_BATCH');
463
+ const needProxy = !(useFacetResolver && useFacetInstall && useBatchFacet);
464
+ const fetchFn = needProxy ? self.buildFetchFn(onProgress) : undefined;
465
+ if (!needProxy) {
466
+ onProgress?.(`[npm] Lazy fetch-proxy: skipped (all facet paths default-on)`);
467
+ }
468
+ self.npmInstaller = new NpmInstaller(self.sqliteFs, runtimeContext.ctx.storage.sql, {
469
+ esbuild: self.esbuildService,
470
+ bundlePool: self.ensureBundlePool(),
471
+ ctx: runtimeContext.ctx,
472
+ env: runtimeContext.env,
473
+ onProgress,
474
+ fetchFn,
475
+ });
476
+ return self.npmInstaller;
477
+ }
478
+ export function _envFlagDefaultOn(self, runtimeContext, name) {
479
+ const raw = runtimeContext.env?.[name];
480
+ if (raw === undefined || raw === null)
481
+ return true;
482
+ const s = String(raw).toLowerCase();
483
+ if (s === '0' || s === '' || s === 'false' || s === 'off' || s === 'no')
484
+ return false;
485
+ return true;
486
+ }
487
+ export function ensureGlobalPrefixDirs(self, runtimeContext, prefix) {
488
+ const fs = self.sqliteFs.as(CRED_SESSION_USER);
489
+ const dirs = [
490
+ prefix,
491
+ `${prefix}/lib`,
492
+ `${prefix}/lib/node_modules`,
493
+ `${prefix}/bin`,
494
+ ];
495
+ for (const dir of dirs) {
496
+ if (!fs.exists(dir))
497
+ fs.mkdir(dir, { recursive: true });
498
+ }
499
+ }
500
+ export function bindRuntimeServices(host, context) {
501
+ return {
502
+ ensureBundlePool: ensureBundlePool.bind(null, host, context),
503
+ ensureFacetManager: ensureFacetManager.bind(null, host, context),
504
+ _ensureWebSocketRelay: _ensureWebSocketRelay.bind(null, host, context),
505
+ _ensureFacetProcessManager: _ensureFacetProcessManager.bind(null, host, context),
506
+ ensureFetchProxy: ensureFetchProxy.bind(null, host, context),
507
+ buildFetchFn: buildFetchFn.bind(null, host, context),
508
+ ensureNpmInstaller: ensureNpmInstaller.bind(null, host, context),
509
+ _envFlagDefaultOn: _envFlagDefaultOn.bind(null, host, context),
510
+ ensureGlobalPrefixDirs: ensureGlobalPrefixDirs.bind(null, host, context),
511
+ };
512
+ }
@@ -6,9 +6,10 @@
6
6
  * Each spawn receives its own lifecycle envelope while command semantics
7
7
  * continue to flow through the existing supervisor RPC.
8
8
  */
9
+ /** The child ring takes bytes; the inline result is text, encoded at this edge. */
9
10
  export interface SpawnPoolHooks {
10
- onStdout(data: string): void;
11
- onStderr(data: string): void;
11
+ onStdout(data: Uint8Array): void;
12
+ onStderr(data: Uint8Array): void;
12
13
  }
13
14
  export interface SpawnPoolReq {
14
15
  command: string;
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-pool.d.ts","sourceRoot":"","sources":["../../../src/loaders/child-process/spawn-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,qBAAqB;IAChC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC;;;;;OAKG;IACH,OAAO,CAAC,KAAK,CAAuC;gBAExC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,kBAAkB;IAS7C;;;;;;;;OAQG;IACG,MAAM,CACV,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,cAAc,GAAG,cAAc,GAAG,cAAc,EACtD,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,MAAM,CAAC;CA8CnB"}
1
+ {"version":3,"file":"spawn-pool.d.ts","sourceRoot":"","sources":["../../../src/loaders/child-process/spawn-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;CAClC;AAGD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,qBAAqB;IAChC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC;;;;;OAKG;IACH,OAAO,CAAC,KAAK,CAAuC;gBAExC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,kBAAkB;IAS7C;;;;;;;;OAQG;IACG,MAAM,CACV,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,cAAc,GAAG,cAAc,GAAG,cAAc,EACtD,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,MAAM,CAAC;CA8CnB"}
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { IsolatePool } from '@nimbus-sh/fabric/isolate-pool.js';
10
10
  import { runSpawnInIsolate } from './spawn-facet.js';
11
+ const RESULT_ENCODER = new TextEncoder();
11
12
  export class ChildProcessSpawnPool {
12
13
  /**
13
14
  * Shared single-slot pool. Serial dispatch keeps one dynamic-worker
@@ -79,9 +80,9 @@ export class ChildProcessSpawnPool {
79
80
  this.chain = myTurn.catch(() => undefined);
80
81
  result = await myTurn;
81
82
  if (result.stdout)
82
- hooks.onStdout(result.stdout);
83
+ hooks.onStdout(RESULT_ENCODER.encode(result.stdout));
83
84
  if (result.stderr)
84
- hooks.onStderr(result.stderr);
85
+ hooks.onStderr(RESULT_ENCODER.encode(result.stderr));
85
86
  return typeof result.exitCode === 'number' ? result.exitCode : 1;
86
87
  }
87
88
  }
@@ -11,11 +11,11 @@
11
11
  * helpers by name.
12
12
  *
13
13
  * Tar-stream symbols: parseTarHeader, streamTarEntries,
14
- * readableStreamToAsyncIterable, MAX_FILE_BYTES.
14
+ * streamPackageEntries, readableStreamToAsyncIterable, MAX_FILE_BYTES.
15
15
  * W7-frame symbols: encodeWriteBatchStream, decodeWriteBatchStream,
16
16
  * W7_MAGIC, W7_MAX_RECORD_BYTES.
17
17
  *
18
- * Tar size: 4.15 KiB
18
+ * Tar size: 4.62 KiB
19
19
  * W7 size: 28.97 KiB
20
20
  */
21
21
  export declare const TAR_STREAM_PREAMBLE: string;
@@ -1 +1 @@
1
- {"version":3,"file":"generated-workers.d.ts","sourceRoot":"","sources":["../../src/loaders/generated-workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,mBAAmB,EAAE,MAA+1I,CAAC;AAEl4I,eAAO,MAAM,iBAAiB,EAAE,MAA4/7B,CAAC"}
1
+ {"version":3,"file":"generated-workers.d.ts","sourceRoot":"","sources":["../../src/loaders/generated-workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,mBAAmB,EAAE,MAAk1J,CAAC;AAEr3J,eAAO,MAAM,iBAAiB,EAAE,MAA4/7B,CAAC"}