@volter/tabnode 0.5.48 → 0.5.49

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.
@@ -5425,7 +5425,7 @@ function parse(urlString, parseQueryString = false, slashesDenoteHost = false) {
5425
5425
  hostname: null,
5426
5426
  hash: __hash,
5427
5427
  search: __search,
5428
- query: parseQueryString ? Object.fromEntries(new URLSearchParams(__search || "")) : __search ? __search.slice(1) : null,
5428
+ query: parseQueryString ? Object.fromEntries(new URLSearchParams$1(__search || "")) : __search ? __search.slice(1) : null,
5429
5429
  pathname: __pathname || null,
5430
5430
  path: __pathname + (__search || "") || null,
5431
5431
  href: urlString
@@ -5499,7 +5499,7 @@ function format(urlObject) {
5499
5499
  if (typeof urlObject.query === "string") {
5500
5500
  result += "?" + urlObject.query;
5501
5501
  } else {
5502
- const params = new URLSearchParams();
5502
+ const params = new URLSearchParams$1();
5503
5503
  for (const [key, value] of Object.entries(urlObject.query)) {
5504
5504
  if (Array.isArray(value)) {
5505
5505
  for (const v2 of value) {
@@ -5528,7 +5528,7 @@ function resolve$2(from, to2) {
5528
5528
  }
5529
5529
  }
5530
5530
  const URL$1 = globalThis.URL;
5531
- const URLSearchParams = globalThis.URLSearchParams;
5531
+ const URLSearchParams$1 = globalThis.URLSearchParams;
5532
5532
  function fileURLToPath(url2) {
5533
5533
  const urlObj = typeof url2 === "string" ? new globalThis.URL(url2) : url2;
5534
5534
  if (urlObj.protocol !== "file:") {
@@ -5551,7 +5551,7 @@ var url$1 = {
5551
5551
  format,
5552
5552
  resolve: resolve$2,
5553
5553
  URL: URL$1,
5554
- URLSearchParams,
5554
+ URLSearchParams: URLSearchParams$1,
5555
5555
  URLPattern: URLPattern$1,
5556
5556
  fileURLToPath,
5557
5557
  pathToFileURL
@@ -5560,7 +5560,7 @@ var urlShim = /* @__PURE__ */ Object.freeze({
5560
5560
  __proto__: null,
5561
5561
  URL: URL$1,
5562
5562
  URLPattern: URLPattern$1,
5563
- URLSearchParams,
5563
+ URLSearchParams: URLSearchParams$1,
5564
5564
  default: url$1,
5565
5565
  fileURLToPath,
5566
5566
  format,
@@ -21191,10 +21191,17 @@ var implementation$1 = function bind2(that) {
21191
21191
  var implementation = implementation$1;
21192
21192
  var functionBind = Function.prototype.bind || implementation;
21193
21193
  var functionCall = Function.prototype.call;
21194
- var functionApply = Function.prototype.apply;
21194
+ var functionApply;
21195
+ var hasRequiredFunctionApply;
21196
+ function requireFunctionApply() {
21197
+ if (hasRequiredFunctionApply) return functionApply;
21198
+ hasRequiredFunctionApply = 1;
21199
+ functionApply = Function.prototype.apply;
21200
+ return functionApply;
21201
+ }
21195
21202
  var reflectApply = typeof Reflect !== "undefined" && Reflect && Reflect.apply;
21196
21203
  var bind$2 = functionBind;
21197
- var $apply$1 = functionApply;
21204
+ var $apply$1 = requireFunctionApply();
21198
21205
  var $call$2 = functionCall;
21199
21206
  var $reflectApply = reflectApply;
21200
21207
  var actualApply = $reflectApply || bind$2.call($call$2, $apply$1);
@@ -21314,7 +21321,7 @@ var hasSymbols = requireHasSymbols()();
21314
21321
  var getProto = requireGetProto();
21315
21322
  var $ObjectGPO = requireObject_getPrototypeOf();
21316
21323
  var $ReflectGPO = requireReflect_getPrototypeOf();
21317
- var $apply = functionApply;
21324
+ var $apply = requireFunctionApply();
21318
21325
  var $call = functionCall;
21319
21326
  var needsEval = {};
21320
21327
  var TypedArray = typeof Uint8Array === "undefined" || !getProto ? undefined$1 : getProto(Uint8Array);
@@ -21955,7 +21962,7 @@ function requireApplyBind() {
21955
21962
  if (hasRequiredApplyBind) return applyBind;
21956
21963
  hasRequiredApplyBind = 1;
21957
21964
  var bind3 = functionBind;
21958
- var $apply2 = functionApply;
21965
+ var $apply2 = requireFunctionApply();
21959
21966
  var actualApply$1 = actualApply;
21960
21967
  applyBind = function applyBind2() {
21961
21968
  return actualApply$1(bind3, $apply2, arguments);
@@ -32263,6 +32270,68 @@ function guestPromise(owner) {
32263
32270
  }
32264
32271
  return constructor;
32265
32272
  }
32273
+ const kIsDisturbed = Symbol.for("nodejs.stream.disturbed");
32274
+ function isAsyncIterableBody(body) {
32275
+ if (body === null || typeof body !== "object") return false;
32276
+ if (typeof ReadableStream === "function" && body instanceof ReadableStream) return false;
32277
+ if (typeof Blob === "function" && body instanceof Blob) return false;
32278
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return false;
32279
+ if (typeof FormData === "function" && body instanceof FormData) return false;
32280
+ if (body instanceof URLSearchParams) return false;
32281
+ return typeof body[Symbol.asyncIterator] === "function";
32282
+ }
32283
+ function refuseDisturbed(body) {
32284
+ const stream = body;
32285
+ const disturbed = stream[kIsDisturbed] !== void 0 ? Boolean(stream[kIsDisturbed]) : Boolean(stream.readableDidRead || stream.readableAborted);
32286
+ if (disturbed || stream.locked === true) throw new TypeError("Response body object should not be disturbed or locked");
32287
+ }
32288
+ function streamFrom(iterable) {
32289
+ let iterator;
32290
+ return new ReadableStream({
32291
+ async start() {
32292
+ iterator = iterable[Symbol.asyncIterator]();
32293
+ },
32294
+ async pull(controller) {
32295
+ for (; ; ) {
32296
+ const { done, value } = await iterator.next();
32297
+ if (done) {
32298
+ controller.close();
32299
+ return;
32300
+ }
32301
+ const bytes2 = Buffer$9.isBuffer(value) ? value : Buffer$9.from(value);
32302
+ if (bytes2.byteLength) {
32303
+ controller.enqueue(new Uint8Array(bytes2));
32304
+ return;
32305
+ }
32306
+ }
32307
+ },
32308
+ async cancel() {
32309
+ var _a3;
32310
+ await ((_a3 = iterator == null ? void 0 : iterator.return) == null ? void 0 : _a3.call(iterator));
32311
+ }
32312
+ }, { highWaterMark: 0 });
32313
+ }
32314
+ const REQUEST_INIT_MEMBERS = ["method", "headers", "referrer", "referrerPolicy", "mode", "credentials", "cache", "redirect", "integrity", "keepalive", "signal", "window", "duplex", "priority"];
32315
+ function withNodeRequestBody(init) {
32316
+ if (!init) return init;
32317
+ const body = init.body;
32318
+ if (!isAsyncIterableBody(body)) return init;
32319
+ refuseDisturbed(body);
32320
+ const copy2 = {};
32321
+ for (const member of Object.keys(init)) if (member !== "body") copy2[member] = init[member];
32322
+ for (const member of REQUEST_INIT_MEMBERS) {
32323
+ if (member in copy2) continue;
32324
+ const value = init[member];
32325
+ if (value !== void 0) copy2[member] = value;
32326
+ }
32327
+ copy2.body = streamFrom(body);
32328
+ return copy2;
32329
+ }
32330
+ function nodeResponseBody(body) {
32331
+ if (!isAsyncIterableBody(body)) return body;
32332
+ refuseDisturbed(body);
32333
+ return streamFrom(body);
32334
+ }
32266
32335
  const staticRequestBodies = /* @__PURE__ */ new WeakSet();
32267
32336
  function rememberRequestBodySource(request, input, init) {
32268
32337
  if (!request.body) return;
@@ -32272,6 +32341,11 @@ function rememberRequestBodySource(request, input, init) {
32272
32341
  }
32273
32342
  function guestFetch(process2, fallback) {
32274
32343
  return function fetch2(input, init) {
32344
+ try {
32345
+ init = withNodeRequestBody(init);
32346
+ } catch (error) {
32347
+ return Promise.reject(error);
32348
+ }
32275
32349
  return fallback.call(globalThis, input, init);
32276
32350
  };
32277
32351
  }
@@ -32310,7 +32384,8 @@ function installNodeResponse(target) {
32310
32384
  const Native = target.Response;
32311
32385
  if (typeof Native !== "function" || Native[INSTALLED]) return;
32312
32386
  const proto = Native.prototype;
32313
- const nativeHeaders = (_a3 = Object.getOwnPropertyDescriptor(proto, "headers")) == null ? void 0 : _a3.get;
32387
+ let nativeHeaders;
32388
+ for (let at3 = proto; at3 && !nativeHeaders; at3 = Object.getPrototypeOf(at3)) nativeHeaders = (_a3 = Object.getOwnPropertyDescriptor(at3, "headers")) == null ? void 0 : _a3.get;
32314
32389
  const nativeFormData = proto.formData;
32315
32390
  const nativeBlob = proto.blob;
32316
32391
  const nativeArrayBuffer = proto.arrayBuffer;
@@ -32320,7 +32395,7 @@ function installNodeResponse(target) {
32320
32395
  const NodeResponse = function Response2(body, init) {
32321
32396
  if (!new.target) throw new TypeError("Class constructor Response cannot be invoked without 'new'");
32322
32397
  const given = givenHeaders(init);
32323
- const response = Reflect.construct(Native, [body, given ? withHeaders(init, given) : init], new.target === NodeResponse ? Native : new.target);
32398
+ const response = Reflect.construct(Native, [nodeResponseBody(body), given ? withHeaders(init, given) : init], new.target === NodeResponse ? Native : new.target);
32324
32399
  kept.set(response, withCookies(nativeHeaders.call(response), given));
32325
32400
  return response;
32326
32401
  };
@@ -74207,6 +74282,63 @@ function installShellJobs(bash, host) {
74207
74282
  if (shell.jobs.size === 0) shells$1.delete(id2);
74208
74283
  return { stdout: held.stdout, stderr: held.stderr + stderr, exitCode: status };
74209
74284
  }));
74285
+ for (const shell of ["sh", "bash"]) {
74286
+ bash.registerCommand(uA(shell, async (args, ctx) => {
74287
+ var _a3;
74288
+ const exec = ctx.exec;
74289
+ if (!exec) return { stdout: "", stderr: `bash: ${shell}: needs a shell that can run a script
74290
+ `, exitCode: 1 };
74291
+ let script;
74292
+ let zero2;
74293
+ let positional;
74294
+ let sourced;
74295
+ let at3 = 0;
74296
+ while (at3 < args.length && /^[-+][a-bd-zA-Z]+$/u.test(args[at3])) at3++;
74297
+ args = args.slice(at3);
74298
+ if (args[0] === "-c" && args.length >= 2) {
74299
+ script = args[1];
74300
+ zero2 = args[2] ?? shell;
74301
+ positional = args.slice(3);
74302
+ } else if (args.length === 0) {
74303
+ if (!((_a3 = ctx.stdin) == null ? void 0 : _a3.trim())) return { stdout: "", stderr: "", exitCode: 0 };
74304
+ script = ctx.stdin;
74305
+ zero2 = shell;
74306
+ positional = [];
74307
+ } else {
74308
+ const path2 = ctx.fs.resolvePath(ctx.cwd, args[0]);
74309
+ try {
74310
+ script = String(await ctx.fs.readFile(path2));
74311
+ } catch {
74312
+ return { stdout: "", stderr: `${shell}: ${args[0]}: No such file or directory
74313
+ `, exitCode: 127 };
74314
+ }
74315
+ zero2 = args[0];
74316
+ positional = args.slice(1);
74317
+ sourced = path2;
74318
+ }
74319
+ if (script.startsWith("#!")) {
74320
+ const newline = script.indexOf("\n");
74321
+ if (newline !== -1) script = script.slice(newline + 1);
74322
+ }
74323
+ const env = { ...ctx.exportedEnv ?? {}, "0": zero2, "#": String(positional.length), "@": positional.join(" "), "*": positional.join(" ") };
74324
+ positional.forEach((word, index2) => {
74325
+ env[String(index2 + 1)] = word;
74326
+ });
74327
+ const token = ctx.env.get(PROCESS_TOKEN_ENV);
74328
+ if (token) env[PROCESS_TOKEN_ENV] = token;
74329
+ if (sourced !== void 0) script = `. ${sourced.replace(/'/gu, "'\\''").replace(/^(.*)$/su, "'$1'")}`;
74330
+ const result = await exec(script, { env, cwd: ctx.cwd, stdin: ctx.stdin, signal: ctx.signal });
74331
+ return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: result.exitCode ?? 0 };
74332
+ }));
74333
+ }
74334
+ bash.registerCommand(uA("exec", async (args, ctx) => {
74335
+ if (args.length === 0) return { stdout: "", stderr: "", exitCode: 0 };
74336
+ if (!ctx.exec) return { stdout: "", stderr: "bash: exec: needs a shell that can run a line\n", exitCode: 1 };
74337
+ const line = args.map((word) => /^[A-Za-z0-9_./:=@%+,-]+$/u.test(word) ? word : `'${word.replace(/'/gu, "'\\''")}'`).join(" ");
74338
+ const env = Object.fromEntries(ctx.env);
74339
+ const result = await ctx.exec(line, { env, cwd: ctx.cwd, replaceEnv: true });
74340
+ return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: result.exitCode ?? 0 };
74341
+ }));
74210
74342
  bash.registerCommand(uA("kill", async (args) => {
74211
74343
  let signal = "SIGTERM";
74212
74344
  let index2 = 0;
@@ -74388,6 +74520,18 @@ function registerRunStreams(token, streams2) {
74388
74520
  }
74389
74521
  function releaseRunStreams(token) {
74390
74522
  _runStreams.delete(token);
74523
+ shellRuns.delete(token);
74524
+ hostedNodes.delete(token);
74525
+ }
74526
+ const shellRuns = /* @__PURE__ */ new Set();
74527
+ const hostedNodes = /* @__PURE__ */ new Set();
74528
+ function nestedNodeToken(parentToken, args, cwd) {
74529
+ var _a3;
74530
+ const token = `child-${__nextChildRun++}`;
74531
+ setRunPid(token, mintPid(), ((_a3 = runPid(parentToken)) == null ? void 0 : _a3.pid) ?? 0, { argv: ["node", ...args], ...cwd ? { cwd } : {} });
74532
+ const parentStreams = runStreamsFor(parentToken);
74533
+ if (parentStreams) registerRunStreams(token, parentStreams);
74534
+ return token;
74391
74535
  }
74392
74536
  function runStreamsFor(token) {
74393
74537
  return _runStreams.get(token);
@@ -74456,19 +74600,26 @@ function initChildProcess(vfs2) {
74456
74600
  if (!tree2) {
74457
74601
  return { stdout: "", stderr: "VFS not initialized\n", exitCode: 1 };
74458
74602
  }
74459
- const runToken = runTokenOf(ctx);
74603
+ const parentToken = runTokenOf(ctx);
74604
+ const nested = parentToken !== null && (shellRuns.has(parentToken) || hostedNodes.has(parentToken));
74605
+ const runToken = nested ? nestedNodeToken(parentToken, args, ctx.cwd) : parentToken;
74460
74606
  const streams2 = runToken === null ? void 0 : runStreamsFor(runToken);
74461
74607
  const processHost = nodeProcessHostFor();
74462
74608
  if (processHost && runToken !== null) {
74463
- return runHostedNode(processHost, {
74464
- token: runToken,
74465
- argv: [...args],
74466
- cwd: ctx.cwd,
74467
- filesystem: tree2,
74468
- env: guestEnvironmentOf(ctx),
74469
- ...typeof ctx.stdin === "string" ? { stdin: ctx.stdin } : {},
74470
- ...streams2 ? { streams: streams2 } : {}
74471
- });
74609
+ if (!nested) hostedNodes.add(runToken);
74610
+ try {
74611
+ return await runHostedNode(processHost, {
74612
+ token: runToken,
74613
+ argv: [...args],
74614
+ cwd: ctx.cwd,
74615
+ filesystem: tree2,
74616
+ env: guestEnvironmentOf(ctx),
74617
+ ...typeof ctx.stdin === "string" ? { stdin: ctx.stdin } : {},
74618
+ ...streams2 ? { streams: streams2 } : {}
74619
+ });
74620
+ } finally {
74621
+ if (nested) releaseRunStreams(runToken);
74622
+ }
74472
74623
  }
74473
74624
  const execArgv = [];
74474
74625
  let first = 0;
@@ -75354,6 +75505,7 @@ function existsInTree(path2) {
75354
75505
  return false;
75355
75506
  }
75356
75507
  }
75508
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set(["sh", "bash", "dash", "/bin/sh", "/bin/bash", "/usr/bin/sh", "/usr/bin/bash"]);
75357
75509
  function engineProgramFor(file, cwd) {
75358
75510
  const name = __substrateProgramName(file);
75359
75511
  if (!name.includes("/")) return name;
@@ -75400,6 +75552,7 @@ function startChildRun(request) {
75400
75552
  const controller = new AbortController();
75401
75553
  const pendingStdin = [];
75402
75554
  const admittedNode = nodeProcessHostInstalled();
75555
+ shellRuns.add(token);
75403
75556
  const hostTerminal = request.terminal !== void 0 && hostExecutor() !== null && !admittedNode;
75404
75557
  let wakeInput;
75405
75558
  let liveInput = false;
@@ -75498,7 +75651,8 @@ function startChildRun(request) {
75498
75651
  const begin = () => {
75499
75652
  if (started || finished) return;
75500
75653
  started = true;
75501
- const engineFirst = !hostTerminal && programExists(request.file, request.cwd, request.env);
75654
+ const shellChild = SHELL_PROGRAMS.has(engineProgramFor(request.file, request.cwd)) && hostExecutor() !== null;
75655
+ const engineFirst = !hostTerminal && !shellChild && programExists(request.file, request.cwd, request.env);
75502
75656
  liveInput = !hostTerminal && !admittedNode && request.stdinIsPipe && !engineFirst;
75503
75657
  if (liveInput) {
75504
75658
  pendingStdin.unshift(...initialStdin);
@@ -80548,6 +80702,7 @@ forGuestRealm(() => {
80548
80702
  const kept = /* @__PURE__ */ new WeakMap();
80549
80703
  class NodeRequest extends Native {
80550
80704
  constructor(input, init) {
80705
+ init = withNodeRequestBody(init);
80551
80706
  super(input, init);
80552
80707
  rememberRequestBodySource(this, input, init);
80553
80708
  const asked = new Headers((init == null ? void 0 : init.headers) ?? (input instanceof Native ? input.headers : void 0));
@@ -1,4 +1,3 @@
1
- /** Host Fetch I/O uses the same process-owned liveness registry as sockets. */
2
1
  import type { Process } from './shims/process';
3
2
  export interface FetchActivity {
4
3
  /** One pending transport operation; the returned release is idempotent. */
package/dist/index.mjs CHANGED
@@ -5132,7 +5132,7 @@ function parse$1(urlString, parseQueryString = false, slashesDenoteHost = false)
5132
5132
  hostname: null,
5133
5133
  hash: __hash,
5134
5134
  search: __search,
5135
- query: parseQueryString ? Object.fromEntries(new URLSearchParams(__search || "")) : __search ? __search.slice(1) : null,
5135
+ query: parseQueryString ? Object.fromEntries(new URLSearchParams$1(__search || "")) : __search ? __search.slice(1) : null,
5136
5136
  pathname: __pathname || null,
5137
5137
  path: __pathname + (__search || "") || null,
5138
5138
  href: urlString
@@ -5206,7 +5206,7 @@ function format$1(urlObject) {
5206
5206
  if (typeof urlObject.query === "string") {
5207
5207
  result += "?" + urlObject.query;
5208
5208
  } else {
5209
- const params = new URLSearchParams();
5209
+ const params = new URLSearchParams$1();
5210
5210
  for (const [key, value] of Object.entries(urlObject.query)) {
5211
5211
  if (Array.isArray(value)) {
5212
5212
  for (const v of value) {
@@ -5235,7 +5235,7 @@ function resolve$2(from, to) {
5235
5235
  }
5236
5236
  }
5237
5237
  const URL$1 = globalThis.URL;
5238
- const URLSearchParams = globalThis.URLSearchParams;
5238
+ const URLSearchParams$1 = globalThis.URLSearchParams;
5239
5239
  function fileURLToPath(url2) {
5240
5240
  const urlObj = typeof url2 === "string" ? new globalThis.URL(url2) : url2;
5241
5241
  if (urlObj.protocol !== "file:") {
@@ -5258,7 +5258,7 @@ const url$1 = {
5258
5258
  format: format$1,
5259
5259
  resolve: resolve$2,
5260
5260
  URL: URL$1,
5261
- URLSearchParams,
5261
+ URLSearchParams: URLSearchParams$1,
5262
5262
  URLPattern: URLPattern$1,
5263
5263
  fileURLToPath,
5264
5264
  pathToFileURL
@@ -5267,7 +5267,7 @@ const urlShim = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePrope
5267
5267
  __proto__: null,
5268
5268
  URL: URL$1,
5269
5269
  URLPattern: URLPattern$1,
5270
- URLSearchParams,
5270
+ URLSearchParams: URLSearchParams$1,
5271
5271
  default: url$1,
5272
5272
  fileURLToPath,
5273
5273
  format: format$1,
@@ -32424,6 +32424,68 @@ function guestPromise(owner) {
32424
32424
  }
32425
32425
  return constructor;
32426
32426
  }
32427
+ const kIsDisturbed = Symbol.for("nodejs.stream.disturbed");
32428
+ function isAsyncIterableBody(body) {
32429
+ if (body === null || typeof body !== "object") return false;
32430
+ if (typeof ReadableStream === "function" && body instanceof ReadableStream) return false;
32431
+ if (typeof Blob === "function" && body instanceof Blob) return false;
32432
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return false;
32433
+ if (typeof FormData === "function" && body instanceof FormData) return false;
32434
+ if (body instanceof URLSearchParams) return false;
32435
+ return typeof body[Symbol.asyncIterator] === "function";
32436
+ }
32437
+ function refuseDisturbed(body) {
32438
+ const stream = body;
32439
+ const disturbed = stream[kIsDisturbed] !== void 0 ? Boolean(stream[kIsDisturbed]) : Boolean(stream.readableDidRead || stream.readableAborted);
32440
+ if (disturbed || stream.locked === true) throw new TypeError("Response body object should not be disturbed or locked");
32441
+ }
32442
+ function streamFrom(iterable) {
32443
+ let iterator;
32444
+ return new ReadableStream({
32445
+ async start() {
32446
+ iterator = iterable[Symbol.asyncIterator]();
32447
+ },
32448
+ async pull(controller) {
32449
+ for (; ; ) {
32450
+ const { done, value } = await iterator.next();
32451
+ if (done) {
32452
+ controller.close();
32453
+ return;
32454
+ }
32455
+ const bytes2 = Buffer$9.isBuffer(value) ? value : Buffer$9.from(value);
32456
+ if (bytes2.byteLength) {
32457
+ controller.enqueue(new Uint8Array(bytes2));
32458
+ return;
32459
+ }
32460
+ }
32461
+ },
32462
+ async cancel() {
32463
+ var _a2;
32464
+ await ((_a2 = iterator == null ? void 0 : iterator.return) == null ? void 0 : _a2.call(iterator));
32465
+ }
32466
+ }, { highWaterMark: 0 });
32467
+ }
32468
+ const REQUEST_INIT_MEMBERS = ["method", "headers", "referrer", "referrerPolicy", "mode", "credentials", "cache", "redirect", "integrity", "keepalive", "signal", "window", "duplex", "priority"];
32469
+ function withNodeRequestBody(init) {
32470
+ if (!init) return init;
32471
+ const body = init.body;
32472
+ if (!isAsyncIterableBody(body)) return init;
32473
+ refuseDisturbed(body);
32474
+ const copy2 = {};
32475
+ for (const member of Object.keys(init)) if (member !== "body") copy2[member] = init[member];
32476
+ for (const member of REQUEST_INIT_MEMBERS) {
32477
+ if (member in copy2) continue;
32478
+ const value = init[member];
32479
+ if (value !== void 0) copy2[member] = value;
32480
+ }
32481
+ copy2.body = streamFrom(body);
32482
+ return copy2;
32483
+ }
32484
+ function nodeResponseBody(body) {
32485
+ if (!isAsyncIterableBody(body)) return body;
32486
+ refuseDisturbed(body);
32487
+ return streamFrom(body);
32488
+ }
32427
32489
  let transport;
32428
32490
  const staticRequestBodies = /* @__PURE__ */ new WeakSet();
32429
32491
  function rememberRequestBodySource(request, input, init) {
@@ -32478,14 +32540,19 @@ function activity(owner, cancel) {
32478
32540
  function guestFetch(process2, fallback) {
32479
32541
  return function fetch2(input, init) {
32480
32542
  const adapter = transport;
32481
- if (!adapter) return fallback.call(globalThis, input, init);
32482
- const owner = __tokenForProcess(process2);
32543
+ const owner = adapter ? __tokenForProcess(process2) : null;
32483
32544
  const ended = () => {
32484
32545
  var _a2;
32485
32546
  return owner !== null && ((_a2 = __runFor(owner)) == null ? void 0 : _a2.process) !== process2;
32486
32547
  };
32487
32548
  const stopped2 = () => new DOMException("The requesting process has ended.", "AbortError");
32488
- if (ended()) return Promise.reject(stopped2());
32549
+ if (adapter && ended()) return Promise.reject(stopped2());
32550
+ try {
32551
+ init = withNodeRequestBody(init);
32552
+ } catch (error) {
32553
+ return Promise.reject(error);
32554
+ }
32555
+ if (!adapter) return fallback.call(globalThis, input, init);
32489
32556
  const context2 = { begin(cancel) {
32490
32557
  if (ended()) throw stopped2();
32491
32558
  return activity(owner, cancel);
@@ -32529,7 +32596,8 @@ function installNodeResponse(target) {
32529
32596
  const Native = target.Response;
32530
32597
  if (typeof Native !== "function" || Native[INSTALLED]) return;
32531
32598
  const proto = Native.prototype;
32532
- const nativeHeaders = (_a2 = Object.getOwnPropertyDescriptor(proto, "headers")) == null ? void 0 : _a2.get;
32599
+ let nativeHeaders;
32600
+ for (let at2 = proto; at2 && !nativeHeaders; at2 = Object.getPrototypeOf(at2)) nativeHeaders = (_a2 = Object.getOwnPropertyDescriptor(at2, "headers")) == null ? void 0 : _a2.get;
32533
32601
  const nativeFormData = proto.formData;
32534
32602
  const nativeBlob = proto.blob;
32535
32603
  const nativeArrayBuffer = proto.arrayBuffer;
@@ -32539,7 +32607,7 @@ function installNodeResponse(target) {
32539
32607
  const NodeResponse = function Response(body, init) {
32540
32608
  if (!new.target) throw new TypeError("Class constructor Response cannot be invoked without 'new'");
32541
32609
  const given = givenHeaders(init);
32542
- const response = Reflect.construct(Native, [body, given ? withHeaders(init, given) : init], new.target === NodeResponse ? Native : new.target);
32610
+ const response = Reflect.construct(Native, [nodeResponseBody(body), given ? withHeaders(init, given) : init], new.target === NodeResponse ? Native : new.target);
32543
32611
  kept.set(response, withCookies(nativeHeaders.call(response), given));
32544
32612
  return response;
32545
32613
  };
@@ -33809,6 +33877,63 @@ function installShellJobs(bash, host2) {
33809
33877
  if (shell.jobs.size === 0) shells$1.delete(id);
33810
33878
  return { stdout: held.stdout, stderr: held.stderr + stderr, exitCode: status };
33811
33879
  }));
33880
+ for (const shell of ["sh", "bash"]) {
33881
+ bash.registerCommand(defineCommand(shell, async (args, ctx) => {
33882
+ var _a2;
33883
+ const exec = ctx.exec;
33884
+ if (!exec) return { stdout: "", stderr: `bash: ${shell}: needs a shell that can run a script
33885
+ `, exitCode: 1 };
33886
+ let script;
33887
+ let zero2;
33888
+ let positional;
33889
+ let sourced;
33890
+ let at2 = 0;
33891
+ while (at2 < args.length && /^[-+][a-bd-zA-Z]+$/u.test(args[at2])) at2++;
33892
+ args = args.slice(at2);
33893
+ if (args[0] === "-c" && args.length >= 2) {
33894
+ script = args[1];
33895
+ zero2 = args[2] ?? shell;
33896
+ positional = args.slice(3);
33897
+ } else if (args.length === 0) {
33898
+ if (!((_a2 = ctx.stdin) == null ? void 0 : _a2.trim())) return { stdout: "", stderr: "", exitCode: 0 };
33899
+ script = ctx.stdin;
33900
+ zero2 = shell;
33901
+ positional = [];
33902
+ } else {
33903
+ const path2 = ctx.fs.resolvePath(ctx.cwd, args[0]);
33904
+ try {
33905
+ script = String(await ctx.fs.readFile(path2));
33906
+ } catch {
33907
+ return { stdout: "", stderr: `${shell}: ${args[0]}: No such file or directory
33908
+ `, exitCode: 127 };
33909
+ }
33910
+ zero2 = args[0];
33911
+ positional = args.slice(1);
33912
+ sourced = path2;
33913
+ }
33914
+ if (script.startsWith("#!")) {
33915
+ const newline = script.indexOf("\n");
33916
+ if (newline !== -1) script = script.slice(newline + 1);
33917
+ }
33918
+ const env = { ...ctx.exportedEnv ?? {}, "0": zero2, "#": String(positional.length), "@": positional.join(" "), "*": positional.join(" ") };
33919
+ positional.forEach((word, index2) => {
33920
+ env[String(index2 + 1)] = word;
33921
+ });
33922
+ const token = ctx.env.get(PROCESS_TOKEN_ENV);
33923
+ if (token) env[PROCESS_TOKEN_ENV] = token;
33924
+ if (sourced !== void 0) script = `. ${sourced.replace(/'/gu, "'\\''").replace(/^(.*)$/su, "'$1'")}`;
33925
+ const result = await exec(script, { env, cwd: ctx.cwd, stdin: ctx.stdin, signal: ctx.signal });
33926
+ return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: result.exitCode ?? 0 };
33927
+ }));
33928
+ }
33929
+ bash.registerCommand(defineCommand("exec", async (args, ctx) => {
33930
+ if (args.length === 0) return { stdout: "", stderr: "", exitCode: 0 };
33931
+ if (!ctx.exec) return { stdout: "", stderr: "bash: exec: needs a shell that can run a line\n", exitCode: 1 };
33932
+ const line = args.map((word) => /^[A-Za-z0-9_./:=@%+,-]+$/u.test(word) ? word : `'${word.replace(/'/gu, "'\\''")}'`).join(" ");
33933
+ const env = Object.fromEntries(ctx.env);
33934
+ const result = await ctx.exec(line, { env, cwd: ctx.cwd, replaceEnv: true });
33935
+ return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: result.exitCode ?? 0 };
33936
+ }));
33812
33937
  bash.registerCommand(defineCommand("kill", async (args) => {
33813
33938
  let signal = "SIGTERM";
33814
33939
  let index2 = 0;
@@ -34009,6 +34134,18 @@ function registerRunStreams(token, streams2) {
34009
34134
  }
34010
34135
  function releaseRunStreams(token) {
34011
34136
  _runStreams.delete(token);
34137
+ shellRuns.delete(token);
34138
+ hostedNodes.delete(token);
34139
+ }
34140
+ const shellRuns = /* @__PURE__ */ new Set();
34141
+ const hostedNodes = /* @__PURE__ */ new Set();
34142
+ function nestedNodeToken(parentToken, args, cwd) {
34143
+ var _a2;
34144
+ const token = `child-${__nextChildRun++}`;
34145
+ setRunPid(token, mintPid(), ((_a2 = runPid(parentToken)) == null ? void 0 : _a2.pid) ?? 0, { argv: ["node", ...args], ...cwd ? { cwd } : {} });
34146
+ const parentStreams = runStreamsFor(parentToken);
34147
+ if (parentStreams) registerRunStreams(token, parentStreams);
34148
+ return token;
34012
34149
  }
34013
34150
  function runStreamsFor(token) {
34014
34151
  return _runStreams.get(token);
@@ -34100,19 +34237,26 @@ function initChildProcess(vfs2) {
34100
34237
  if (!tree2) {
34101
34238
  return { stdout: "", stderr: "VFS not initialized\n", exitCode: 1 };
34102
34239
  }
34103
- const runToken = runTokenOf(ctx);
34240
+ const parentToken = runTokenOf(ctx);
34241
+ const nested = parentToken !== null && (shellRuns.has(parentToken) || hostedNodes.has(parentToken));
34242
+ const runToken = nested ? nestedNodeToken(parentToken, args, ctx.cwd) : parentToken;
34104
34243
  const streams2 = runToken === null ? void 0 : runStreamsFor(runToken);
34105
34244
  const processHost = nodeProcessHostFor(runToken);
34106
34245
  if (processHost && runToken !== null) {
34107
- return runHostedNode(processHost, {
34108
- token: runToken,
34109
- argv: [...args],
34110
- cwd: ctx.cwd,
34111
- filesystem: tree2,
34112
- env: guestEnvironmentOf(ctx),
34113
- ...typeof ctx.stdin === "string" ? { stdin: ctx.stdin } : {},
34114
- ...streams2 ? { streams: streams2 } : {}
34115
- });
34246
+ if (!nested) hostedNodes.add(runToken);
34247
+ try {
34248
+ return await runHostedNode(processHost, {
34249
+ token: runToken,
34250
+ argv: [...args],
34251
+ cwd: ctx.cwd,
34252
+ filesystem: tree2,
34253
+ env: guestEnvironmentOf(ctx),
34254
+ ...typeof ctx.stdin === "string" ? { stdin: ctx.stdin } : {},
34255
+ ...streams2 ? { streams: streams2 } : {}
34256
+ });
34257
+ } finally {
34258
+ if (nested) releaseRunStreams(runToken);
34259
+ }
34116
34260
  }
34117
34261
  const execArgv = [];
34118
34262
  let first = 0;
@@ -35018,6 +35162,7 @@ function existsInTree(path2) {
35018
35162
  return false;
35019
35163
  }
35020
35164
  }
35165
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set(["sh", "bash", "dash", "/bin/sh", "/bin/bash", "/usr/bin/sh", "/usr/bin/bash"]);
35021
35166
  function engineProgramFor(file, cwd) {
35022
35167
  const name = __substrateProgramName(file);
35023
35168
  if (!name.includes("/")) return name;
@@ -35064,6 +35209,7 @@ function startChildRun(request) {
35064
35209
  const controller = new AbortController();
35065
35210
  const pendingStdin = [];
35066
35211
  const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === "node";
35212
+ if (!admittedNode) shellRuns.add(token);
35067
35213
  const hostTerminal = request.terminal !== void 0 && hostExecutor() !== null && !admittedNode;
35068
35214
  let wakeInput;
35069
35215
  let liveInput = false;
@@ -35162,7 +35308,8 @@ function startChildRun(request) {
35162
35308
  const begin = () => {
35163
35309
  if (started2 || finished) return;
35164
35310
  started2 = true;
35165
- const engineFirst = !hostTerminal && programExists(request.file, request.cwd, request.env);
35311
+ const shellChild = SHELL_PROGRAMS.has(engineProgramFor(request.file, request.cwd)) && hostExecutor() !== null;
35312
+ const engineFirst = !hostTerminal && !shellChild && programExists(request.file, request.cwd, request.env);
35166
35313
  liveInput = !hostTerminal && !admittedNode && request.stdinIsPipe && !engineFirst;
35167
35314
  if (liveInput) {
35168
35315
  pendingStdin.unshift(...initialStdin);
@@ -40220,6 +40367,7 @@ forGuestRealm(() => {
40220
40367
  const kept = /* @__PURE__ */ new WeakMap();
40221
40368
  class NodeRequest extends Native {
40222
40369
  constructor(input, init) {
40370
+ init = withNodeRequestBody(init);
40223
40371
  super(input, init);
40224
40372
  rememberRequestBodySource(this, input, init);
40225
40373
  const asked = new Headers((init == null ? void 0 : init.headers) ?? (input instanceof Native ? input.headers : void 0));
@@ -40548,7 +40696,7 @@ class WorkerRuntime {
40548
40696
  this.worker = new Worker(
40549
40697
  new URL(
40550
40698
  /* @vite-ignore */
40551
- "/assets/runtime-worker-B-BP9mj-.js",
40699
+ "/assets/runtime-worker-C1IV4qnr.js",
40552
40700
  import.meta.url
40553
40701
  ),
40554
40702
  { type: "module" }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The init with an async-iterable body read as a stream, or the init itself.
3
+ * The body is read once; the replacement is built member by member (an init
4
+ * may carry its members on getters of its prototype, as a `Request` passed as
5
+ * an init does), with the init's own other members as they are.
6
+ */
7
+ export declare function withNodeRequestBody<T extends RequestInit | undefined>(init: T): T;
8
+ /** A response body as undici takes it: an async iterable read as a stream, anything else as given. */
9
+ export declare function nodeResponseBody(body: unknown): unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/tabnode",
3
- "version": "0.5.48",
3
+ "version": "0.5.49",
4
4
  "description": "Node's own library, in a browser tab.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
1
  /** Host Fetch I/O uses the same process-owned liveness registry as sockets. */
2
+ import { withNodeRequestBody } from './node-body';
2
3
  import type { Process } from './shims/process';
3
4
  import { __tokenForProcess, __runFor, enterRun, type ProcessToken } from './process-tokens';
4
5
  import { registerHandle, __adoptHandle, refHandle, unrefHandle, releaseHandle } from './node-lib/binding/handles';
@@ -83,11 +84,14 @@ function activity(owner: ProcessToken | null, cancel: () => void): FetchActivity
83
84
  export function guestFetch(process: Process, fallback: typeof globalThis.fetch): typeof globalThis.fetch {
84
85
  return function fetch(input, init) {
85
86
  const adapter = transport;
86
- if (!adapter) return fallback.call(globalThis, input, init);
87
- const owner = __tokenForProcess(process);
87
+ const owner = adapter ? __tokenForProcess(process) : null;
88
88
  const ended = () => owner !== null && __runFor(owner)?.process !== process;
89
89
  const stopped = () => new DOMException('The requesting process has ended.', 'AbortError');
90
- if (ended()) return Promise.reject(stopped());
90
+ if (adapter && ended()) return Promise.reject(stopped());
91
+ // A stream body is read as undici reads it, whichever door the fetch
92
+ // leaves by; a body undici refuses rejects the fetch, as Node's does.
93
+ try { init = withNodeRequestBody(init); } catch (error) { return Promise.reject(error); }
94
+ if (!adapter) return fallback.call(globalThis, input, init);
91
95
  const context: FetchTransportContext = { begin(cancel) {
92
96
  if (ended()) throw stopped();
93
97
  return activity(owner, cancel);
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A body as Node's fetch (undici's `extractBody`) accepts it: besides what a
3
+ * browser takes, any async iterable (a Node `Readable`, an `http.IncomingMessage`,
4
+ * an async generator) is read as a stream of its chunks, each a `Buffer`,
5
+ * a typed array or a string (UTF-8). A browser's `Request` and `Response` take
6
+ * such an object for a string: `new Request(url, { method: 'POST', body:
7
+ * incomingMessage, duplex: 'half' })` has the body "[object Object]". Next's
8
+ * App Router builds every route handler's request exactly that way (its
9
+ * request adapter passes the Node request as the body, "handled by undici"),
10
+ * so every POST a route handler read was that string, and NextAuth's
11
+ * credentials callback found no CSRF token in it.
12
+ *
13
+ * Only such a body is changed; every other init is handed on untouched.
14
+ */
15
+ import { Buffer } from './node-lib/buffer-module';
16
+
17
+ /** The mark Node's streams carry once read (`internal/streams/utils`, `kIsDisturbed`). */
18
+ const kIsDisturbed = Symbol.for('nodejs.stream.disturbed');
19
+
20
+ function isAsyncIterableBody(body: unknown): body is AsyncIterable<unknown> {
21
+ if (body === null || typeof body !== 'object') return false;
22
+ if (typeof ReadableStream === 'function' && body instanceof ReadableStream) return false;
23
+ if (typeof Blob === 'function' && body instanceof Blob) return false;
24
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return false;
25
+ if (typeof FormData === 'function' && body instanceof FormData) return false;
26
+ if (body instanceof URLSearchParams) return false;
27
+ return typeof (body as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function';
28
+ }
29
+
30
+ /** Node's `isDisturbed`, and undici's refusal of a body already read or locked. */
31
+ function refuseDisturbed(body: object): void {
32
+ const stream = body as { locked?: unknown; readableDidRead?: unknown; readableAborted?: unknown; [kIsDisturbed]?: unknown };
33
+ const disturbed = stream[kIsDisturbed] !== undefined ? Boolean(stream[kIsDisturbed]) : Boolean(stream.readableDidRead || stream.readableAborted);
34
+ if (disturbed || stream.locked === true) throw new TypeError('Response body object should not be disturbed or locked');
35
+ }
36
+
37
+ /**
38
+ * undici's `ReadableStreamFrom`: the iterator made when the stream starts,
39
+ * each chunk `Buffer.from`'d as Node's is (a chunk `Buffer.from` refuses
40
+ * errors the stream), an empty chunk passed over for the next, and the
41
+ * iterator returned, with no argument, on cancel.
42
+ */
43
+ function streamFrom(iterable: AsyncIterable<unknown>): ReadableStream<Uint8Array> {
44
+ let iterator: AsyncIterator<unknown> | undefined;
45
+ return new ReadableStream<Uint8Array>({
46
+ async start() { iterator = iterable[Symbol.asyncIterator](); },
47
+ async pull(controller) {
48
+ for (;;) {
49
+ const { done, value } = await iterator!.next();
50
+ if (done) { controller.close(); return; }
51
+ const bytes = (Buffer.isBuffer(value) ? value : (Buffer.from as (chunk: unknown) => Uint8Array)(value)) as Uint8Array;
52
+ if (bytes.byteLength) { controller.enqueue(new Uint8Array(bytes)); return; }
53
+ }
54
+ },
55
+ async cancel() { await iterator?.return?.(); },
56
+ }, { highWaterMark: 0 });
57
+ }
58
+
59
+ const REQUEST_INIT_MEMBERS = ['method', 'headers', 'referrer', 'referrerPolicy', 'mode', 'credentials', 'cache', 'redirect', 'integrity', 'keepalive', 'signal', 'window', 'duplex', 'priority'] as const;
60
+
61
+ /**
62
+ * The init with an async-iterable body read as a stream, or the init itself.
63
+ * The body is read once; the replacement is built member by member (an init
64
+ * may carry its members on getters of its prototype, as a `Request` passed as
65
+ * an init does), with the init's own other members as they are.
66
+ */
67
+ export function withNodeRequestBody<T extends RequestInit | undefined>(init: T): T {
68
+ if (!init) return init;
69
+ const body = (init as RequestInit).body as unknown;
70
+ if (!isAsyncIterableBody(body)) return init;
71
+ refuseDisturbed(body);
72
+ const copy: Record<string, unknown> = {};
73
+ for (const member of Object.keys(init)) if (member !== 'body') copy[member] = (init as Record<string, unknown>)[member];
74
+ for (const member of REQUEST_INIT_MEMBERS) {
75
+ if (member in copy) continue;
76
+ const value = (init as Record<string, unknown>)[member];
77
+ if (value !== undefined) copy[member] = value;
78
+ }
79
+ copy.body = streamFrom(body);
80
+ return copy as T;
81
+ }
82
+
83
+ /** A response body as undici takes it: an async iterable read as a stream, anything else as given. */
84
+ export function nodeResponseBody(body: unknown): unknown {
85
+ if (!isAsyncIterableBody(body)) return body;
86
+ refuseDisturbed(body);
87
+ return streamFrom(body);
88
+ }
@@ -1,4 +1,5 @@
1
1
  import { defineOnHost, takeFromHost } from './host-globals';
2
+ import { nodeResponseBody } from './node-body';
2
3
 
3
4
  /**
4
5
  * A web `Response` that keeps its `Set-Cookie` headers, as Node's does.
@@ -76,7 +77,9 @@ export function installNodeResponse(target: { Response?: typeof Response }): voi
76
77
  const Native = target.Response as ResponseConstructor | undefined;
77
78
  if (typeof Native !== 'function' || Native[INSTALLED]) return;
78
79
  const proto = Native.prototype;
79
- const nativeHeaders = Object.getOwnPropertyDescriptor(proto, 'headers')?.get;
80
+ // The getter is the prototype's own in a browser; found up the chain wherever it is.
81
+ let nativeHeaders: (() => unknown) | undefined;
82
+ for (let at: object | null = proto; at && !nativeHeaders; at = Object.getPrototypeOf(at)) nativeHeaders = Object.getOwnPropertyDescriptor(at, 'headers')?.get;
80
83
  const nativeFormData = proto.formData;
81
84
  const nativeBlob = proto.blob;
82
85
  const nativeArrayBuffer = proto.arrayBuffer;
@@ -87,7 +90,7 @@ export function installNodeResponse(target: { Response?: typeof Response }): voi
87
90
  const NodeResponse = function Response(this: unknown, body?: BodyInit | null, init?: ResponseInit): Response {
88
91
  if (!new.target) throw new TypeError("Class constructor Response cannot be invoked without 'new'");
89
92
  const given = givenHeaders(init);
90
- const response = Reflect.construct(Native, [body, given ? withHeaders(init, given) : init], (new.target as unknown) === NodeResponse ? Native : new.target) as Response;
93
+ const response = Reflect.construct(Native, [nodeResponseBody(body) as BodyInit | null | undefined, given ? withHeaders(init, given) : init], (new.target as unknown) === NodeResponse ? Native : new.target) as Response;
91
94
  kept.set(response, withCookies(nativeHeaders.call(response) as Headers, given));
92
95
  return response;
93
96
  } as unknown as ResponseConstructor;
package/src/runtime.ts CHANGED
@@ -9,6 +9,7 @@ import { VirtualFS } from './virtual-fs';
9
9
  import { guestPromise, intrinsicPromise } from './promise-ownership';
10
10
  import { guestFetch, rememberRequestBodySource } from './fetch-transport';
11
11
  import { installNodeResponse } from './node-response';
12
+ import { withNodeRequestBody } from './node-body';
12
13
  import { forGuestRealm, installGuestRealm, takeFromHost, defineOnHost, heldWork } from './host-globals';
13
14
  import type { IRuntime, IExecuteResult, IRuntimeOptions } from './runtime-interface';
14
15
  import type { PackageJson } from './types/package-json';
@@ -2953,6 +2954,8 @@ forGuestRealm(() => {
2953
2954
  const kept = new WeakMap<object, Headers>();
2954
2955
  class NodeRequest extends Native {
2955
2956
  constructor(input: RequestInfo | URL, init?: RequestInit) {
2957
+ // An async-iterable body (a Node stream) is read as undici reads it.
2958
+ init = withNodeRequestBody(init);
2956
2959
  super(input as RequestInfo, init);
2957
2960
  rememberRequestBodySource(this, input, init);
2958
2961
  const asked = new Headers(init?.headers ?? (input instanceof Native ? (input as Request).headers : undefined));
@@ -268,6 +268,22 @@ export function registerRunStreams(token: ProcessToken, streams: RunStreams): vo
268
268
  /** Forget a run that has ended. */
269
269
  export function releaseRunStreams(token: ProcessToken): void {
270
270
  _runStreams.delete(token);
271
+ shellRuns.delete(token);
272
+ hostedNodes.delete(token);
273
+ }
274
+
275
+ /** Runs whose program is a shell or another non-node program: a `node` they run is their child. */
276
+ const shellRuns = new Set<ProcessToken>();
277
+ /** Runs that are themselves a hosted `node`: a further `node` under them is a child. */
278
+ const hostedNodes = new Set<ProcessToken>();
279
+
280
+ /** A child identity for a `node` a run's shell runs: its own token and pid under the run's, on the run's streams. */
281
+ function nestedNodeToken(parentToken: ProcessToken, args: readonly string[], cwd: string | undefined): ProcessToken {
282
+ const token: ProcessToken = `child-${__nextChildRun++}`;
283
+ setRunPid(token, mintPid(), runPid(parentToken)?.pid ?? 0, { argv: ['node', ...args], ...(cwd ? { cwd } : {}) });
284
+ const parentStreams = runStreamsFor(parentToken);
285
+ if (parentStreams) registerRunStreams(token, parentStreams);
286
+ return token;
271
287
  }
272
288
 
273
289
  /** What the host gave the named run, while it lasts. */
@@ -429,14 +445,30 @@ export function initChildProcess(vfs: VirtualFS): void {
429
445
 
430
446
  // Capture this run's name, streams and cancellation before another Node
431
447
  // entry can start. Forks and shell entries reach this same dispatch seam.
432
- const runToken = runTokenOf(ctx);
448
+ const parentToken = runTokenOf(ctx);
449
+ // A `node` a shell runs is a process of its own unless it is the run
450
+ // itself: the first `node` of a run that is not a shell's child is the
451
+ // run (the CMD `node server.js`, the page's `node -e`), and every other
452
+ // one, a line of a start script, the second command of a `-c` line, a
453
+ // step of a `sh file` child, gets a child token under the run's pid,
454
+ // the run's streams, and its own admission at the host. Admitted under
455
+ // the shell's own token, its exit was read as the shell's: a script
456
+ // ended at its first `node` line with nothing after it (measured
457
+ // 2026-09-28: `sh start.sh` printed its first step and stopped).
458
+ const nested = parentToken !== null && (shellRuns.has(parentToken) || hostedNodes.has(parentToken));
459
+ const runToken = nested ? nestedNodeToken(parentToken!, args, ctx.cwd) : parentToken;
433
460
  const streams = runToken === null ? undefined : runStreamsFor(runToken);
434
461
  const processHost = nodeProcessHostFor(runToken);
435
462
  if (processHost && runToken !== null) {
436
- return runHostedNode(processHost, {
437
- token: runToken, argv: [...args], cwd: ctx.cwd, filesystem: tree, env: guestEnvironmentOf(ctx),
438
- ...(typeof ctx.stdin === 'string' ? { stdin: ctx.stdin } : {}), ...(streams ? { streams } : {}),
439
- });
463
+ if (!nested) hostedNodes.add(runToken);
464
+ try {
465
+ return await runHostedNode(processHost, {
466
+ token: runToken, argv: [...args], cwd: ctx.cwd, filesystem: tree, env: guestEnvironmentOf(ctx),
467
+ ...(typeof ctx.stdin === 'string' ? { stdin: ctx.stdin } : {}), ...(streams ? { streams } : {}),
468
+ });
469
+ } finally {
470
+ if (nested) releaseRunStreams(runToken);
471
+ }
440
472
  }
441
473
 
442
474
  // Node reads its own options before the script: `node --turbo-fast-api-calls
@@ -1719,6 +1751,9 @@ function existsInTree(path: string): boolean {
1719
1751
  * a program the engine can run; the engine's own `node`, written to
1720
1752
  * `/usr/local/bin/node` when the shim is initialized, is one of those.
1721
1753
  */
1754
+ /** The shells a script or a `-c` line runs under; a child of these goes to the host when there is one. */
1755
+ const SHELL_PROGRAMS = new Set(['sh', 'bash', 'dash', '/bin/sh', '/bin/bash', '/usr/bin/sh', '/usr/bin/bash']);
1756
+
1722
1757
  function engineProgramFor(file: string, cwd?: string): string {
1723
1758
  const name = __substrateProgramName(file);
1724
1759
  if (!name.includes('/')) return name;
@@ -1794,6 +1829,7 @@ function startChildRun(request: RunRequest): StartedRun {
1794
1829
  // A host terminal consumes input incrementally. The old string-only
1795
1830
  // route dropped every keystroke that arrived after a child was launched.
1796
1831
  const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === 'node';
1832
+ if (!admittedNode) shellRuns.add(token);
1797
1833
  const hostTerminal = request.terminal !== undefined && hostExecutor() !== null && !admittedNode;
1798
1834
  let wakeInput: (() => void) | undefined;
1799
1835
  /**
@@ -1904,7 +1940,14 @@ function startChildRun(request: RunRequest): StartedRun {
1904
1940
  const begin = (): void => {
1905
1941
  if (started || finished) return;
1906
1942
  started = true;
1907
- const engineFirst = !hostTerminal && programExists(request.file, request.cwd, request.env);
1943
+ // A shell a hosted program spawns (`sh start.sh`, `sh -c '…'`) runs at
1944
+ // the host when there is one, not in this worker's own bash: the host is
1945
+ // where the programs the page registered live (`npx` under /usr/bin),
1946
+ // and where a `node` inside the script is given a named run. Run here,
1947
+ // the script's `node` found no name and was refused, and its `npx` was
1948
+ // not found at all.
1949
+ const shellChild = SHELL_PROGRAMS.has(engineProgramFor(request.file, request.cwd)) && hostExecutor() !== null;
1950
+ const engineFirst = !hostTerminal && !shellChild && programExists(request.file, request.cwd, request.env);
1908
1951
  liveInput = !hostTerminal && !admittedNode && request.stdinIsPipe && !engineFirst;
1909
1952
  if (liveInput) {
1910
1953
  pendingStdin.unshift(...initialStdin);
@@ -386,6 +386,78 @@ export function installShellJobs(bash: Bash, host: ShellJobsHost): void {
386
386
  return { stdout: held.stdout, stderr: held.stderr + stderr, exitCode: status };
387
387
  }));
388
388
 
389
+ // `sh file`, `sh -c line` and `sh` on stdin: the shell's own ran the script
390
+ // with the caller's exported variables alone, and a run's name travels in
391
+ // a variable the shell never exports, so a `node` on any line of a script
392
+ // found no named run and was refused ("Isolated Node execution requires a
393
+ // named run", a start script in browser-substrate's tab, 2026-09-28). The
394
+ // script is run as the shell's own would run it, with the name carried.
395
+ for (const shell of ['sh', 'bash'] as const) {
396
+ bash.registerCommand(defineCommand(shell, async (args, ctx) => {
397
+ const exec = ctx.exec;
398
+ if (!exec) return { stdout: '', stderr: `bash: ${shell}: needs a shell that can run a script\n`, exitCode: 1 };
399
+ let script: string;
400
+ let zero: string;
401
+ let positional: string[];
402
+ let sourced: string | undefined;
403
+ // Options before the script (`sh -e start.sh`, `bash -eu -c …`) are the
404
+ // shell's own; they are passed over rather than read as the file's name.
405
+ let at = 0;
406
+ while (at < args.length && /^[-+][a-bd-zA-Z]+$/u.test(args[at]!)) at++;
407
+ args = args.slice(at);
408
+ if (args[0] === '-c' && args.length >= 2) {
409
+ script = args[1]!;
410
+ zero = args[2] ?? shell;
411
+ positional = args.slice(3);
412
+ } else if (args.length === 0) {
413
+ if (!ctx.stdin?.trim()) return { stdout: '', stderr: '', exitCode: 0 };
414
+ script = ctx.stdin;
415
+ zero = shell;
416
+ positional = [];
417
+ } else {
418
+ const path = ctx.fs.resolvePath(ctx.cwd, args[0]!);
419
+ try { script = String(await ctx.fs.readFile(path)); }
420
+ catch { return { stdout: '', stderr: `${shell}: ${args[0]}: No such file or directory\n`, exitCode: 127 }; }
421
+ zero = args[0]!;
422
+ positional = args.slice(1);
423
+ sourced = path;
424
+ }
425
+ if (script.startsWith('#!')) {
426
+ const newline = script.indexOf('\n');
427
+ if (newline !== -1) script = script.slice(newline + 1);
428
+ }
429
+ const env: Record<string, string> = { ...(ctx.exportedEnv ?? {}), '0': zero, '#': String(positional.length), '@': positional.join(' '), '*': positional.join(' ') };
430
+ positional.forEach((word, index) => { env[String(index + 1)] = word; });
431
+ const token = ctx.env.get(PROCESS_TOKEN_ENV);
432
+ if (token) env[PROCESS_TOKEN_ENV] = token;
433
+ // A file is sourced by one statement of the nested run rather than run
434
+ // as the nested run's own script: a nested script of several lines
435
+ // stopped after its first hosted `node` in a tab (2026-09-28), while
436
+ // the same lines sourced from one statement ran whole. The shebang
437
+ // line is a comment to the shell that sources it.
438
+ if (sourced !== undefined) script = `. ${sourced.replace(/'/gu, "'\\''").replace(/^(.*)$/su, "'$1'")}`;
439
+ const result = await exec(script, { env, cwd: ctx.cwd, stdin: ctx.stdin, signal: (ctx as { signal?: AbortSignal }).signal });
440
+ return { stdout: result.stdout ?? '', stderr: result.stderr ?? '', exitCode: result.exitCode ?? 0 };
441
+ }));
442
+ }
443
+ // `exec command…` runs the command in the script's place, with the
444
+ // script's environment and its run's name, and answers with its status.
445
+ // The shell's own `exec` ran a builtin but a program the engine defines,
446
+ // `node`, ran nothing: a start script's `exec node server.js` ended with
447
+ // no server and no word. This one hands the line back to the shell, which
448
+ // is where `node`, `npm` and the host's programs are found. The shell is
449
+ // not replaced: a line after `exec` runs once the command ends, where
450
+ // bash would never reach it. A start script's `exec` is its last line, a
451
+ // server that does not end.
452
+ bash.registerCommand(defineCommand('exec', async (args, ctx) => {
453
+ if (args.length === 0) return { stdout: '', stderr: '', exitCode: 0 };
454
+ if (!ctx.exec) return { stdout: '', stderr: 'bash: exec: needs a shell that can run a line\n', exitCode: 1 };
455
+ const line = args.map((word) => /^[A-Za-z0-9_./:=@%+,-]+$/u.test(word) ? word : `'${word.replace(/'/gu, "'\\''")}'`).join(' ');
456
+ const env: Record<string, string> = Object.fromEntries(ctx.env);
457
+ const result = await ctx.exec(line, { env, cwd: ctx.cwd, replaceEnv: true });
458
+ return { stdout: result.stdout ?? '', stderr: result.stderr ?? '', exitCode: result.exitCode ?? 0 };
459
+ }));
460
+
389
461
  bash.registerCommand(defineCommand('kill', async (args) => {
390
462
  let signal = 'SIGTERM';
391
463
  let index = 0;