@poe-platform/safe-bash 0.1.600 → 0.1.602

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 (100) hide show
  1. package/dist/safe-bash/chunks/{chunk-IYFB6VED.js → chunk-4BYUV5PA.js} +6 -4
  2. package/dist/safe-bash/chunks/{chunk-IYFB6VED.js.map → chunk-4BYUV5PA.js.map} +1 -1
  3. package/dist/safe-bash/chunks/{chunk-PE5NZIA2.js → chunk-6LSSEOG7.js} +3 -3
  4. package/dist/safe-bash/chunks/{chunk-3Y5PKSHX.js → chunk-AD7UAP5B.js} +10 -8
  5. package/dist/safe-bash/chunks/{chunk-3Y5PKSHX.js.map → chunk-AD7UAP5B.js.map} +1 -1
  6. package/dist/safe-bash/chunks/{chunk-XBLVOMEJ.js → chunk-AFH57PPL.js} +2 -2
  7. package/dist/safe-bash/chunks/{chunk-FMTBJ7SG.js → chunk-CEPNDODE.js} +3 -3
  8. package/dist/safe-bash/chunks/chunk-CXMIIXJA.js +178 -0
  9. package/dist/safe-bash/chunks/chunk-CXMIIXJA.js.map +7 -0
  10. package/dist/safe-bash/chunks/{chunk-E3NZKSTA.js → chunk-F5TCC4UG.js} +12 -10
  11. package/dist/safe-bash/chunks/{chunk-E3NZKSTA.js.map → chunk-F5TCC4UG.js.map} +1 -1
  12. package/dist/safe-bash/chunks/{chunk-TJVKGNKA.js → chunk-FWDMJTQO.js} +9 -7
  13. package/dist/safe-bash/chunks/{chunk-TJVKGNKA.js.map → chunk-FWDMJTQO.js.map} +1 -1
  14. package/dist/safe-bash/chunks/{chunk-LHKAA57T.js → chunk-GFZKQIPL.js} +8 -6
  15. package/dist/safe-bash/chunks/{chunk-LHKAA57T.js.map → chunk-GFZKQIPL.js.map} +1 -1
  16. package/dist/safe-bash/chunks/{chunk-W3TLNFDB.js → chunk-HHJSJWUD.js} +9 -7
  17. package/dist/safe-bash/chunks/{chunk-W3TLNFDB.js.map → chunk-HHJSJWUD.js.map} +1 -1
  18. package/dist/safe-bash/chunks/{chunk-XYVCXUGM.js → chunk-KZ7QCYNT.js} +10 -8
  19. package/dist/safe-bash/chunks/{chunk-XYVCXUGM.js.map → chunk-KZ7QCYNT.js.map} +1 -1
  20. package/dist/safe-bash/chunks/{chunk-HQCBEAFK.js → chunk-N5CYTGBN.js} +10 -8
  21. package/dist/safe-bash/chunks/{chunk-HQCBEAFK.js.map → chunk-N5CYTGBN.js.map} +1 -1
  22. package/dist/safe-bash/chunks/{chunk-H6E6F5KJ.js → chunk-PCLOK32J.js} +9 -7
  23. package/dist/safe-bash/chunks/{chunk-H6E6F5KJ.js.map → chunk-PCLOK32J.js.map} +1 -1
  24. package/dist/safe-bash/chunks/{chunk-BXVGCJXW.js → chunk-PFMSKAFQ.js} +12 -10
  25. package/dist/safe-bash/chunks/{chunk-BXVGCJXW.js.map → chunk-PFMSKAFQ.js.map} +1 -1
  26. package/dist/safe-bash/chunks/{chunk-PMBBUNNN.js → chunk-PW4WQSCS.js} +11 -9
  27. package/dist/safe-bash/chunks/{chunk-PMBBUNNN.js.map → chunk-PW4WQSCS.js.map} +1 -1
  28. package/dist/safe-bash/chunks/{chunk-OECKXHVJ.js → chunk-RNKTWFEM.js} +2 -2
  29. package/dist/safe-bash/chunks/{chunk-2PQ3XOCQ.js → chunk-UYX72YHY.js} +4 -172
  30. package/dist/safe-bash/chunks/chunk-UYX72YHY.js.map +7 -0
  31. package/dist/safe-bash/chunks/{chunk-QAZN7SAN.js → chunk-WPEMUXV4.js} +7 -5
  32. package/dist/safe-bash/chunks/{chunk-QAZN7SAN.js.map → chunk-WPEMUXV4.js.map} +1 -1
  33. package/dist/safe-bash/commands/csplit/index.browser.js +5 -4
  34. package/dist/safe-bash/commands/factor/index.browser.js +4 -3
  35. package/dist/safe-bash/commands/getopt/index.browser.js +4 -3
  36. package/dist/safe-bash/commands/hexdump/index.browser.js +4 -3
  37. package/dist/safe-bash/commands/iconv/index.browser.js +4 -3
  38. package/dist/safe-bash/commands/line-endings/index.browser.js +5 -4
  39. package/dist/safe-bash/commands/llm/index.browser.js +5 -4
  40. package/dist/safe-bash/commands/network/index.browser.js +6 -5
  41. package/dist/safe-bash/commands/op/index.browser.js +5664 -0
  42. package/dist/safe-bash/commands/op/index.browser.js.map +7 -0
  43. package/dist/safe-bash/commands/op/index.d.ts +14 -0
  44. package/dist/safe-bash/commands/op/index.d.ts.map +1 -0
  45. package/dist/safe-bash/commands/op/index.js +4684 -0
  46. package/dist/safe-bash/commands/op/index.js.map +7 -0
  47. package/dist/safe-bash/commands/op/op.test.d.ts +2 -0
  48. package/dist/safe-bash/commands/op/op.test.d.ts.map +1 -0
  49. package/dist/safe-bash/commands/op/op.test.js +159 -0
  50. package/dist/safe-bash/commands/op/op.test.js.map +1 -0
  51. package/dist/safe-bash/commands/pr/index.browser.js +4 -3
  52. package/dist/safe-bash/commands/tsort/index.browser.js +4 -3
  53. package/dist/safe-bash/commands/xml/index.browser.js +5 -4
  54. package/dist/safe-bash/commands/yq/index.browser.js +6 -5
  55. package/dist/safe-bash/core.browser.js +455 -101
  56. package/dist/safe-bash/core.browser.js.map +4 -4
  57. package/dist/safe-bash/index.d.ts +1 -0
  58. package/dist/safe-bash/index.d.ts.map +1 -1
  59. package/dist/safe-bash/index.js +1 -0
  60. package/dist/safe-bash/index.js.map +1 -1
  61. package/dist/safe-bash/internal/op/admin.d.ts +31 -0
  62. package/dist/safe-bash/internal/op/backend.d.ts +8 -0
  63. package/dist/safe-bash/internal/op/cli.d.ts +80 -0
  64. package/dist/safe-bash/internal/op/documents.d.ts +6 -0
  65. package/dist/safe-bash/internal/op/encoding.d.ts +15 -0
  66. package/dist/safe-bash/internal/op/environment-commands.d.ts +11 -0
  67. package/dist/safe-bash/internal/op/environment.d.ts +9 -0
  68. package/dist/safe-bash/internal/op/file-mode.d.ts +1 -0
  69. package/dist/safe-bash/internal/op/host-contracts.d.ts +12 -0
  70. package/dist/safe-bash/internal/op/index.d.ts +19 -0
  71. package/dist/safe-bash/internal/op/items.d.ts +3 -0
  72. package/dist/safe-bash/internal/op/references.d.ts +4 -0
  73. package/dist/safe-bash/internal/op/secrets.d.ts +13 -0
  74. package/dist/safe-bash/internal/op/templates.d.ts +10 -0
  75. package/dist/safe-bash/internal/op/types.d.ts +202 -0
  76. package/dist/safe-bash/shell/arrays/state.js +1 -1
  77. package/dist/safe-bash/shell/arrays/state.js.map +1 -1
  78. package/dist/safe-bash/shell/background-jobs.d.ts +39 -0
  79. package/dist/safe-bash/shell/background-jobs.d.ts.map +1 -0
  80. package/dist/safe-bash/shell/background-jobs.js +85 -0
  81. package/dist/safe-bash/shell/background-jobs.js.map +1 -0
  82. package/dist/safe-bash/shell/display.js +7 -7
  83. package/dist/safe-bash/shell/display.js.map +1 -1
  84. package/dist/safe-bash/shell/parser.d.ts +1 -0
  85. package/dist/safe-bash/shell/parser.d.ts.map +1 -1
  86. package/dist/safe-bash/shell/parser.js +8 -8
  87. package/dist/safe-bash/shell/parser.js.map +1 -1
  88. package/dist/safe-bash/shell/runtime.d.ts +7 -1
  89. package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
  90. package/dist/safe-bash/shell/runtime.js +295 -46
  91. package/dist/safe-bash/shell/runtime.js.map +1 -1
  92. package/dist/safe-bash/shell/shell.d.ts.map +1 -1
  93. package/dist/safe-bash/shell/shell.js +11 -2
  94. package/dist/safe-bash/shell/shell.js.map +1 -1
  95. package/package.json +9 -2
  96. package/dist/safe-bash/chunks/chunk-2PQ3XOCQ.js.map +0 -7
  97. /package/dist/safe-bash/chunks/{chunk-PE5NZIA2.js.map → chunk-6LSSEOG7.js.map} +0 -0
  98. /package/dist/safe-bash/chunks/{chunk-XBLVOMEJ.js.map → chunk-AFH57PPL.js.map} +0 -0
  99. /package/dist/safe-bash/chunks/{chunk-FMTBJ7SG.js.map → chunk-CEPNDODE.js.map} +0 -0
  100. /package/dist/safe-bash/chunks/{chunk-OECKXHVJ.js.map → chunk-RNKTWFEM.js.map} +0 -0
@@ -1,3 +1,4 @@
1
+ import { BackgroundJobs, BackgroundResources, backgroundResources, backgroundPrune, backgroundDestination, forwardBackgroundDestination } from "./background-jobs.js";
1
2
  import { PublicDiagnostic, publicDiagnosticMessage } from "../diagnostics.js";
2
3
  import { writeDiagnostic } from "../escaping.js";
3
4
  import { cancelTurn, monotonicNow, registerYieldCheckpoint, scheduleTurn, yieldTurn } from "../contracts/yield.js";
@@ -62,7 +63,7 @@ export const defaultLimits = {
62
63
  };
63
64
  const shellBuiltinNames = new Set([
64
65
  ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read", "declare", "mapfile", "readarray",
65
- "exit", "return", "break", "continue", "command", "builtin", "type", "readonly", "echo", "printf", "test", "[", ".", "source", "eval", "getopts", "let", "pushd", "dirs", "popd", "shopt",
66
+ "wait", "exit", "return", "break", "continue", "command", "builtin", "type", "readonly", "echo", "printf", "test", "[", ".", "source", "eval", "getopts", "let", "pushd", "dirs", "popd", "shopt",
66
67
  ]);
67
68
  const implementedBuiltins = new Set([...shellBuiltinNames].filter(name => !["echo", "printf", "test", "["].includes(name)));
68
69
  const specialBuiltinNames = new Set([":", ".", "break", "continue", "eval", "exit", "export", "readonly", "return", "set", "shift", "unset"]);
@@ -81,6 +82,8 @@ export function resolveLimits(...limits) {
81
82
  }
82
83
  return result;
83
84
  }
85
+ const runtimeFileSystems = new WeakMap();
86
+ const inputScopes = new WeakMap();
84
87
  const budgetedSinks = new WeakMap();
85
88
  async function sortExpansionStrings(values, work) {
86
89
  const compare = async (left, right) => {
@@ -249,6 +252,7 @@ export class Budget {
249
252
  await interruptible(sink.write(chunk), signal);
250
253
  },
251
254
  };
255
+ forwardBackgroundDestination(output, sink);
252
256
  budgetedSinks.set(output, { budget: this, write: output.write, ...(ownership?.write === sink.write && ownership.file ? { file: ownership.file } : {}) });
253
257
  return output;
254
258
  }
@@ -425,6 +429,7 @@ function signalSink(sink, signal) {
425
429
  } } : {}),
426
430
  async write(chunk) { signal.throwIfAborted(); await interruptible(write(chunk), signal); },
427
431
  };
432
+ forwardBackgroundDestination(output, sink);
428
433
  if (owned)
429
434
  budgetedSinks.set(output, { ...owned, write: output.write });
430
435
  return output;
@@ -441,6 +446,7 @@ function bindCommandIO(context) {
441
446
  async function cloneState(state, signal, scope, inheritLocals = true) {
442
447
  const destination = await snapshotState(state, () => ({
443
448
  ...state,
449
+ ...(state.backgroundJobs ? { backgroundJobs: new BackgroundJobs(state.backgroundJobs.execution) } : {}),
444
450
  variables: Object.assign(Object.create(null), state.variables),
445
451
  exported: new Set(state.exported), functions: new Map(state.functions), positional: [...state.positional],
446
452
  readonlyVariables: new Set(state.readonlyVariables),
@@ -1147,6 +1153,7 @@ export class Runtime {
1147
1153
  this.outcomeFrame = outcomeFrame;
1148
1154
  this.inputProfile = inputProfile;
1149
1155
  this.fs = scopeFileSystem(fs, () => budget.fileSystemOperation(), signal, () => budget.fileSystemCleanupOperation());
1156
+ runtimeFileSystems.set(this.fs, runtimeFileSystems.get(fs) ?? fs);
1150
1157
  const checkpoint = () => budget.cpuCheckpoint();
1151
1158
  registerYieldCheckpoint(signal, checkpoint);
1152
1159
  registerYieldCheckpoint(commandSignal, checkpoint);
@@ -1934,6 +1941,68 @@ export class Runtime {
1934
1941
  }
1935
1942
  async script(script, state, io) {
1936
1943
  for (const list of script.lists) {
1944
+ if (list.background) {
1945
+ const jobs = state.backgroundJobs;
1946
+ const id = jobs.execution.identifier();
1947
+ const scope = jobs.execution.scope.child();
1948
+ const inheritedDestinations = new Set([io.stdout, io.stderr, ...[...io.descriptors?.values() ?? []].flatMap(descriptor => descriptor.output && !descriptor.closed ? [descriptor.output] : [])].map(backgroundDestination));
1949
+ const leases = (io[backgroundResources] ?? []).filter(resource => !resource.destination || inheritedDestinations.has(resource.destination)).map(resource => ({ resource, release: resource.retain() }));
1950
+ const releases = leases.map(lease => lease.release);
1951
+ let child;
1952
+ try {
1953
+ child = await cloneState(state, this.signal, scope);
1954
+ }
1955
+ catch (error) {
1956
+ releases.forEach(release => release());
1957
+ await scope.close();
1958
+ throw error;
1959
+ }
1960
+ child.isolated = true;
1961
+ child.loopDepth = 0;
1962
+ const inherited = isolateIO(activeIO(io));
1963
+ const redirectsInput = list.pipelines[0].commands[0].redirects.some(redirect => redirect.descriptor === 0);
1964
+ const stdin = redirectsInput ? inherited.stdin : toByteSource("");
1965
+ const descriptors = new Map(inherited.descriptors);
1966
+ descriptors.set(0, { input: stdin, stdinIsDefault: false });
1967
+ let initialRedirection = true;
1968
+ const backgroundIO = { ...inherited, [invocationScope]: scope, stdin, stdinIsDefault: false, descriptors,
1969
+ [backgroundPrune]: list.pipelines.length === 1 ? descriptors => {
1970
+ if (!initialRedirection)
1971
+ return;
1972
+ initialRedirection = false;
1973
+ const destinations = new Set([...descriptors.values()].flatMap(descriptor => descriptor.output && !descriptor.closed ? [backgroundDestination(descriptor.output)] : []));
1974
+ for (const lease of leases)
1975
+ if (lease.resource.destination && !destinations.has(lease.resource.destination))
1976
+ lease.release();
1977
+ } : undefined,
1978
+ };
1979
+ const runtime = new Runtime(runtimeFileSystems.get(this.fs) ?? this.fs, this.commands, this.middleware, this.budget, AbortSignal.any([this.commandSignal, scope.signal]), this.fileWrites, this.outputFiles, this.commandSignal, this.cancellation, this.cancellationState, this.cancellationOwner, this.cancellationDepth, this.cancellationMaxDepth, undefined, this.inputProfile);
1980
+ try {
1981
+ state.lastBackgroundJob = jobs.launch(id, async () => {
1982
+ try {
1983
+ try {
1984
+ return (await runtime.runUnit({ lists: [{ ...list, background: false }] }, child, backgroundIO)).exitCode;
1985
+ }
1986
+ catch (error) {
1987
+ if (error instanceof Flow && error.kind === "return")
1988
+ return error.status;
1989
+ throw error;
1990
+ }
1991
+ }
1992
+ finally {
1993
+ releases.forEach(release => release());
1994
+ await scope.close();
1995
+ }
1996
+ });
1997
+ }
1998
+ catch (error) {
1999
+ releases.forEach(release => release());
2000
+ await scope.close();
2001
+ throw error;
2002
+ }
2003
+ state.status = 0;
2004
+ continue;
2005
+ }
1937
2006
  for (let index = 0; index < list.pipelines.length; index++) {
1938
2007
  const operator = list.operators[index - 1];
1939
2008
  if ((operator === "&&" && state.status !== 0) || (operator === "||" && state.status === 0))
@@ -1979,6 +2048,7 @@ export class Runtime {
1979
2048
  const completed = new Set();
1980
2049
  const closing = new Set();
1981
2050
  let statuses;
2051
+ const stageLeases = [];
1982
2052
  try {
1983
2053
  for (let index = 1; index < pipeline.commands.length; index++)
1984
2054
  pipes.push(createBytePipe({
@@ -1986,7 +2056,11 @@ export class Runtime {
1986
2056
  }));
1987
2057
  for (let index = 0; index < pipeline.commands.length; index++)
1988
2058
  controllers.push(new AbortController());
2059
+ stageLeases.push(...pipeline.commands.map(() => (io[backgroundResources] ?? []).filter(resource => resource.destination).map(resource => ({ resource, release: resource.retain() }))));
2060
+ io[backgroundPrune]?.(new Map());
1989
2061
  const tasks = pipeline.commands.map(async (command, index) => {
2062
+ let initialRedirection = true;
2063
+ const inheritedLeases = stageLeases[index];
1990
2064
  const incoming = pipes[index - 1];
1991
2065
  const outgoing = pipes[index];
1992
2066
  const childDepth = this.cancellationDepth + 1;
@@ -2022,6 +2096,8 @@ export class Runtime {
2022
2096
  throw error;
2023
2097
  }
2024
2098
  } };
2099
+ const resources = new BackgroundResources();
2100
+ resources.destination = backgroundDestination(pipeOutput ?? io.stdout);
2025
2101
  const executeStage = async () => {
2026
2102
  try {
2027
2103
  let exitCode;
@@ -2030,6 +2106,16 @@ export class Runtime {
2030
2106
  child.isolated = true;
2031
2107
  const work = runtime.runCommandIsolated(command, child, {
2032
2108
  ...isolateIO(io),
2109
+ [backgroundResources]: [...io[backgroundResources] ?? [], resources],
2110
+ [backgroundPrune]: descriptors => {
2111
+ if (!initialRedirection)
2112
+ return;
2113
+ initialRedirection = false;
2114
+ const destinations = new Set([...descriptors.values()].flatMap(descriptor => descriptor.output && !descriptor.closed ? [backgroundDestination(descriptor.output)] : []));
2115
+ for (const lease of inheritedLeases)
2116
+ if (!destinations.has(lease.resource.destination))
2117
+ lease.release();
2118
+ },
2033
2119
  stdin: input,
2034
2120
  ...(incoming ? { stdinIsDefault: false } : {}),
2035
2121
  stdout: pipeOutput ? this.budget.sink(pipeOutput, signal) : signalSink(io.stdout, signal),
@@ -2037,6 +2123,8 @@ export class Runtime {
2037
2123
  }).finally(() => stateMonitor(child)?.closeValues());
2038
2124
  retain(work);
2039
2125
  exitCode = await interruptible(work, signal);
2126
+ if (resources.busy)
2127
+ await interruptible(resources.drain(), signal);
2040
2128
  }
2041
2129
  catch (error) {
2042
2130
  if (!(error instanceof PipelineClosed))
@@ -2046,6 +2134,7 @@ export class Runtime {
2046
2134
  return { exitCode };
2047
2135
  }
2048
2136
  finally {
2137
+ inheritedLeases.forEach(lease => lease.release());
2049
2138
  completed.add(index);
2050
2139
  if (incoming) {
2051
2140
  const upstream = index - 1;
@@ -2083,6 +2172,9 @@ export class Runtime {
2083
2172
  }
2084
2173
  finally {
2085
2174
  try {
2175
+ for (const leases of stageLeases)
2176
+ for (const lease of leases)
2177
+ lease.release();
2086
2178
  for (const close of closing)
2087
2179
  cancelTurn(close);
2088
2180
  for (const [index, controller] of controllers.entries())
@@ -2171,13 +2263,22 @@ export class Runtime {
2171
2263
  if (this.budget.commands % 128 === 0)
2172
2264
  await yieldTurn(this.signal);
2173
2265
  this.signal.throwIfAborted();
2266
+ const resources = new BackgroundResources();
2267
+ originalIO = { ...originalIO, [backgroundResources]: [...originalIO[backgroundResources] ?? [], resources] };
2174
2268
  const inputs = new Set();
2175
2269
  const outputs = new Set();
2176
2270
  const finishOutputs = async (status) => {
2177
2271
  const pending = [...outputs];
2178
2272
  outputs.clear();
2179
- const settled = await Promise.allSettled(pending.map(close => close({ status })));
2180
- throwCleanupFailures(settled.filter(result => result.status === "rejected").map(result => result.reason));
2273
+ const finish = async () => {
2274
+ await resources.drain();
2275
+ const settled = await Promise.allSettled(pending.map(close => close({ status })));
2276
+ throwCleanupFailures(settled.filter(result => result.status === "rejected").map(result => result.reason));
2277
+ };
2278
+ if (resources.busy)
2279
+ state.backgroundJobs.execution.track(finish());
2280
+ else
2281
+ await finish();
2181
2282
  };
2182
2283
  const allocation = this.budget.values.scope();
2183
2284
  originalIO = { ...originalIO, [valueScope]: allocation };
@@ -2518,16 +2619,28 @@ export class Runtime {
2518
2619
  }
2519
2620
  finally {
2520
2621
  try {
2521
- await Promise.allSettled([
2522
- ...[...outputs].map(async (close) => close({ reason: new FsError("ECANCELED", { syscall: "redirect" }) })),
2523
- ...[...inputs].map(async (input) => input.close()),
2524
- ]).then(results => {
2525
- const failures = results.filter(result => result.status === "rejected").map(result => result.reason);
2526
- if (diagnosticFailure)
2527
- io[invocationScope].failures.push(...failures);
2528
- else
2529
- throwCleanupFailures(failures);
2530
- }).finally(() => allocation.close());
2622
+ const close = async () => {
2623
+ await resources.drain();
2624
+ await Promise.allSettled([
2625
+ ...[...outputs].map(async (close) => close({ reason: new FsError("ECANCELED", { syscall: "redirect" }) })),
2626
+ ...[...inputs].map(async (input) => { try {
2627
+ await input.close();
2628
+ }
2629
+ finally {
2630
+ await inputScopes.get(input)?.close();
2631
+ } }),
2632
+ ]).then(results => {
2633
+ const failures = results.filter(result => result.status === "rejected").map(result => result.reason);
2634
+ if (diagnosticFailure)
2635
+ io[invocationScope].failures.push(...failures);
2636
+ else
2637
+ throwCleanupFailures(failures);
2638
+ }).finally(() => allocation.close());
2639
+ };
2640
+ if (resources.busy)
2641
+ state.backgroundJobs.execution.track(close());
2642
+ else
2643
+ await close();
2531
2644
  }
2532
2645
  finally {
2533
2646
  finishSnapshot?.();
@@ -2614,6 +2727,8 @@ export class Runtime {
2614
2727
  const stdinIsDefault = descriptor?.input ? descriptor.stdinIsDefault : false;
2615
2728
  return {
2616
2729
  [invocationScope]: io[invocationScope],
2730
+ [backgroundResources]: io[backgroundResources],
2731
+ [backgroundPrune]: io[backgroundPrune],
2617
2732
  ...(io[valueScope] === undefined ? {} : { [valueScope]: io[valueScope] }),
2618
2733
  ...(io.execution === undefined ? {} : { execution: io.execution }),
2619
2734
  ...(io.diagnosticLine === undefined ? {} : { diagnosticLine: io.diagnosticLine }),
@@ -2692,15 +2807,29 @@ export class Runtime {
2692
2807
  const stat = await interruptible(this.fs.stat(path, options), this.signal);
2693
2808
  if (stat.type === "directory" && !fileShortcut)
2694
2809
  throw new PublicDiagnostic(`${target}: Is a directory`);
2695
- const source = stat.type === "directory" ? toByteSource("")
2696
- : await fileInput(this.fs, path, this.budget.limits.maxInputBytes, this.signal, this.inputProfile, { stat, registerCleanup: cleanup => { io[invocationScope].register(cleanup); } });
2697
- const input = new ShellInput(source, this.budget, this.signal, { stat, ...source });
2698
- inputs.add(input);
2810
+ const inputScope = (state.backgroundJobs?.execution.scope ?? io[invocationScope]).child();
2811
+ let input;
2812
+ try {
2813
+ const inputSignal = this.commandSignal;
2814
+ const inputFs = scopeFileSystem(runtimeFileSystems.get(this.fs) ?? this.fs, () => this.budget.fileSystemOperation(), inputSignal, () => this.budget.fileSystemCleanupOperation());
2815
+ const source = stat.type === "directory" ? toByteSource("")
2816
+ : await fileInput(inputFs, path, this.budget.limits.maxInputBytes, inputSignal, this.inputProfile, { stat, registerCleanup: cleanup => { inputScope.register(cleanup); } });
2817
+ input = new ShellInput(source, this.budget, inputSignal, { stat, ...source });
2818
+ inputScopes.set(input, inputScope);
2819
+ inputs.add(input);
2820
+ }
2821
+ catch (error) {
2822
+ await inputScope.close();
2823
+ throw error;
2824
+ }
2699
2825
  descriptors.set(redirect.descriptor, { input, stdinIsDefault: false });
2700
2826
  }
2701
2827
  else {
2828
+ const outputSignal = this.commandSignal;
2829
+ const outputFs = scopeFileSystem(runtimeFileSystems.get(this.fs) ?? this.fs, () => this.budget.fileSystemOperation(), outputSignal, () => this.budget.fileSystemCleanupOperation());
2830
+ const options = { signal: outputSignal };
2702
2831
  const append = redirect.operator === ">>";
2703
- const capabilities = await this.fs.capabilitiesFor?.(path, options) ?? this.fs.capabilities;
2832
+ const capabilities = await outputFs.capabilitiesFor?.(path, options) ?? outputFs.capabilities;
2704
2833
  const random = capabilities.randomAccessWrite === true;
2705
2834
  const key = resolvePath(state.cwd, path);
2706
2835
  let file;
@@ -2710,18 +2839,18 @@ export class Runtime {
2710
2839
  throw new FsError("ENOTSUP", { path, message: "Conflicting sequential output descriptors" });
2711
2840
  file.references++;
2712
2841
  this.outputFiles.set(key, file);
2713
- });
2842
+ }, outputSignal);
2714
2843
  let closed = false;
2715
2844
  let offset = 0;
2716
2845
  const incremental = async () => {
2717
2846
  await this.fileOperation(key, async () => {
2718
2847
  if (append)
2719
- await this.fs.appendFile(path, new Uint8Array(), options);
2848
+ await outputFs.appendFile(path, new Uint8Array(), options);
2720
2849
  else
2721
- await this.fs.writeFile(path, new Uint8Array(), { ...options, flag: "w" });
2850
+ await outputFs.writeFile(path, new Uint8Array(), { ...options, flag: "w" });
2722
2851
  if (!append)
2723
2852
  file.data = new Uint8Array();
2724
- });
2853
+ }, outputSignal);
2725
2854
  return { write: (chunk) => {
2726
2855
  const copy = new Uint8Array(chunk);
2727
2856
  return this.fileOperation(key, async () => {
@@ -2731,18 +2860,18 @@ export class Runtime {
2731
2860
  let atEOF = false;
2732
2861
  if (!append && current && offset === current.length && capabilities.append === true && capabilities.stat !== false) {
2733
2862
  try {
2734
- atEOF = (await interruptible(this.fs.stat(path, options), this.signal)).size === offset;
2863
+ atEOF = (await interruptible(outputFs.stat(path, options), outputSignal)).size === offset;
2735
2864
  }
2736
2865
  catch {
2737
2866
  // Metadata is optional for this optimization; writes need no read access.
2738
- this.signal.throwIfAborted();
2867
+ outputSignal.throwIfAborted();
2739
2868
  }
2740
- this.signal.throwIfAborted();
2869
+ outputSignal.throwIfAborted();
2741
2870
  }
2742
2871
  if (append || atEOF) {
2743
2872
  // Preparing a larger view only touches the unpublished tail of current.
2744
2873
  const bytes = current ? appendOutputBytes(current, copy) : undefined;
2745
- await this.fs.appendFile(path, copy, options);
2874
+ await outputFs.appendFile(path, copy, options);
2746
2875
  file.data = bytes;
2747
2876
  }
2748
2877
  else {
@@ -2750,12 +2879,12 @@ export class Runtime {
2750
2879
  if (current)
2751
2880
  bytes.set(current);
2752
2881
  bytes.set(copy, offset);
2753
- await this.fs.writeFile(path, bytes, options);
2882
+ await outputFs.writeFile(path, bytes, options);
2754
2883
  file.data = bytes;
2755
2884
  }
2756
2885
  if (!append)
2757
2886
  offset += copy.length;
2758
- });
2887
+ }, outputSignal);
2759
2888
  } };
2760
2889
  };
2761
2890
  const resumePathCache = this.budget.pathLookup.suspend();
@@ -2770,8 +2899,8 @@ export class Runtime {
2770
2899
  let target;
2771
2900
  let outputScope;
2772
2901
  try {
2773
- outputScope = io[invocationScope].child();
2774
- target = await openFileOutput({ fs: this.fs, signal: this.signal, registerCleanup: cleanup => { outputScope.register(cleanup); } }, path, append ? "a" : "w", random ? incremental : undefined);
2902
+ outputScope = (state.backgroundJobs?.execution.scope ?? io[invocationScope]).child();
2903
+ target = await openFileOutput({ fs: outputFs, signal: outputSignal, registerCleanup: cleanup => { outputScope.register(cleanup); } }, path, append ? "a" : "w", random ? incremental : undefined);
2775
2904
  }
2776
2905
  catch (error) {
2777
2906
  try {
@@ -2784,8 +2913,8 @@ export class Runtime {
2784
2913
  }
2785
2914
  outputs.add(async (completion) => {
2786
2915
  try {
2787
- if (this.signal.aborted)
2788
- await target.abort(this.signal.reason);
2916
+ if (outputSignal.aborted)
2917
+ await target.abort(outputSignal.reason);
2789
2918
  else if ("reason" in completion)
2790
2919
  await target.abort(completion.reason);
2791
2920
  else {
@@ -2793,7 +2922,7 @@ export class Runtime {
2793
2922
  await target.finish();
2794
2923
  }
2795
2924
  catch (error) {
2796
- this.signal.throwIfAborted();
2925
+ outputSignal.throwIfAborted();
2797
2926
  if (completion.status === 0)
2798
2927
  throw error;
2799
2928
  }
@@ -2808,7 +2937,7 @@ export class Runtime {
2808
2937
  }
2809
2938
  }
2810
2939
  });
2811
- const output = this.budget.sink(target.sink, this.signal);
2940
+ const output = this.budget.sink(target.sink, outputSignal);
2812
2941
  budgetedSinks.get(output).file = Object.freeze({ path });
2813
2942
  descriptors.set(redirect.descriptor, { output });
2814
2943
  if (redirect.operator === "&>") {
@@ -2823,14 +2952,15 @@ export class Runtime {
2823
2952
  const diagnostic = errorTarget === undefined ? undefined : filesystemDiagnostic(error, errorTarget);
2824
2953
  throw new ExecutionFailure(error, currentIO(), diagnostic);
2825
2954
  }
2955
+ io[backgroundPrune]?.(descriptors);
2826
2956
  return currentIO();
2827
2957
  }
2828
- async fileOperation(path, operation) {
2958
+ async fileOperation(path, operation, signal = this.signal) {
2829
2959
  const previous = this.fileWrites.get(path) ?? Promise.resolve();
2830
- const pending = previous.catch(() => undefined).then(() => { this.signal.throwIfAborted(); return operation(); });
2960
+ const pending = previous.catch(() => undefined).then(() => { signal.throwIfAborted(); return operation(); });
2831
2961
  this.fileWrites.set(path, pending);
2832
2962
  try {
2833
- await interruptible(pending, this.signal);
2963
+ await interruptible(pending, signal);
2834
2964
  }
2835
2965
  finally {
2836
2966
  if (this.fileWrites.get(path) === pending)
@@ -2911,8 +3041,11 @@ export class Runtime {
2911
3041
  };
2912
3042
  let overlayOpen = false;
2913
3043
  try {
2914
- if (snapshotScope)
3044
+ if (snapshotScope) {
3045
+ const jobs = state.backgroundJobs;
2915
3046
  state = await cloneState(state, this.signal, snapshotScope);
3047
+ state.backgroundJobs = jobs;
3048
+ }
2916
3049
  if (words.length) {
2917
3050
  stateMonitor(state)?.openOverlay(previous);
2918
3051
  overlayOpen = true;
@@ -3049,6 +3182,8 @@ export class Runtime {
3049
3182
  async dispatchScoped(name, values, state, io, assignments, bypassFunctions) {
3050
3183
  const { [invocationScope]: scope, ...publicIO } = io;
3051
3184
  Reflect.deleteProperty(publicIO, valueScope);
3185
+ Reflect.deleteProperty(publicIO, backgroundResources);
3186
+ Reflect.deleteProperty(publicIO, backgroundPrune);
3052
3187
  const allocation = this.budget.values.scope();
3053
3188
  scope.register(() => allocation.close());
3054
3189
  const argumentValues = this.admitArguments(values, allocation);
@@ -3101,6 +3236,7 @@ export class Runtime {
3101
3236
  const forwardedValues = getCommandArguments(forwarded);
3102
3237
  const admitted = forwardedValues === argumentValues ? argumentValues : this.admitArguments(forwardedValues.values, allocation);
3103
3238
  const context = { ...forwarded, args: admitted.args, argumentValues: admitted, [invocationScope]: scope,
3239
+ [backgroundResources]: io[backgroundResources], [backgroundPrune]: io[backgroundPrune],
3104
3240
  ...(io[valueScope] === undefined ? {} : { [valueScope]: io[valueScope] }) };
3105
3241
  const previous = new Map();
3106
3242
  const cwd = state.cwd;
@@ -3552,6 +3688,7 @@ export class Runtime {
3552
3688
  directoryStack: { entries: [], bytes: 0 },
3553
3689
  dotglob: false,
3554
3690
  globstar: false,
3691
+ backgroundJobs: new BackgroundJobs(state.backgroundJobs.execution),
3555
3692
  positional: [], arg0: shellValueText(arg0), profile: context.command === "sh" ? "sh" : "bash", status: 0, substitutionStatus: 0, depth: state.depth + 1,
3556
3693
  loopDepth: 0, functionDepth: 0, locals: [], pipefail: false, isolated: true,
3557
3694
  errexit: false,
@@ -3758,6 +3895,8 @@ export class Runtime {
3758
3895
  };
3759
3896
  Reflect.deleteProperty(context, invocationScope);
3760
3897
  Reflect.deleteProperty(context, valueScope);
3898
+ Reflect.deleteProperty(context, backgroundResources);
3899
+ Reflect.deleteProperty(context, backgroundPrune);
3761
3900
  bindCommandIO(context);
3762
3901
  bindFileOutputBudget(context, sink => this.budget.sink(sink, runtime.signal));
3763
3902
  if (argumentValues.values.every(value => typeof value === "string"))
@@ -5024,12 +5163,118 @@ export class Runtime {
5024
5163
  }
5025
5164
  }
5026
5165
  }
5166
+ async waitBuiltin(context, state) {
5167
+ let next = false;
5168
+ let variable;
5169
+ let index = 0;
5170
+ for (; index < context.args.length; index++) {
5171
+ const option = context.args[index];
5172
+ if (option === "--") {
5173
+ index++;
5174
+ break;
5175
+ }
5176
+ if (!option.startsWith("-") || option === "-")
5177
+ break;
5178
+ for (let flag = 1; flag < option.length; flag++) {
5179
+ const letter = option[flag];
5180
+ if (letter === "n")
5181
+ next = true;
5182
+ else if (letter === "f") { /* Processes do not stop in the virtual shell. */ }
5183
+ else if (letter === "p") {
5184
+ variable = option.slice(flag + 1) || context.args[++index];
5185
+ if (variable === undefined) {
5186
+ await writeDiagnostic(context.stderr, "wait: -p: option requires an argument\n");
5187
+ return 2;
5188
+ }
5189
+ if (!/^[a-zA-Z_][a-zA-Z_0-9]*(?:\[.+\])?$/su.test(variable)) {
5190
+ await writeDiagnostic(context.stderr, `wait: ${variable}: not a valid identifier\n`);
5191
+ return 1;
5192
+ }
5193
+ break;
5194
+ }
5195
+ else {
5196
+ await writeDiagnostic(context.stderr, `wait: -${letter}: invalid option\n`);
5197
+ return 2;
5198
+ }
5199
+ }
5200
+ }
5201
+ const target = variable === undefined ? undefined : /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\[(.+)\])?$/su.exec(variable);
5202
+ if (target) {
5203
+ const name = target[1];
5204
+ if (state.readonlyVariables?.has(name)) {
5205
+ await writeDiagnostic(context.stderr, `wait: ${name}: readonly variable\n`);
5206
+ return 1;
5207
+ }
5208
+ if (target[2] === undefined) {
5209
+ if (arrayStore(state)?.get(name))
5210
+ await this.unsetIndexed(state, name);
5211
+ else
5212
+ this.unsetVariable(state, name);
5213
+ }
5214
+ }
5215
+ const publish = async (id) => {
5216
+ if (!target)
5217
+ return;
5218
+ if (target[2] === undefined)
5219
+ await this.assignVariable(state, target[1], id);
5220
+ else
5221
+ await this.arrayAssignment({ kind: "element", name: target[1], index: { decimal: target[2], source: target[2] }, append: false, value: { offset: 0, parts: [{ kind: "text", value: id, quoted: true }] } }, state, context);
5222
+ };
5223
+ const table = state.backgroundJobs;
5224
+ const ids = context.args.slice(index);
5225
+ const selected = [];
5226
+ let invalid = false;
5227
+ for (const id of ids) {
5228
+ if (!id.startsWith("%") && !/^[0-9]+$/u.test(id)) {
5229
+ await writeDiagnostic(context.stderr, `wait: ${id}: not a pid or valid job spec\n`);
5230
+ if (next)
5231
+ continue;
5232
+ return 1;
5233
+ }
5234
+ const job = table.resolve(id);
5235
+ if (!job) {
5236
+ await writeDiagnostic(context.stderr, `wait: ${id}: not a child of this shell\n`);
5237
+ invalid = true;
5238
+ }
5239
+ else
5240
+ selected.push(job);
5241
+ }
5242
+ if (next) {
5243
+ const eligible = ids.length ? selected.filter(job => !job.waited) : [...table.jobs.values()].filter(job => !job.done && !job.consumed);
5244
+ if (!eligible.length)
5245
+ return 127;
5246
+ const result = await interruptible(Promise.race(eligible.map(async (job) => ({ job, status: await job.promise }))), this.signal);
5247
+ result.job.consumed = true;
5248
+ await publish(result.job.id);
5249
+ return result.status;
5250
+ }
5251
+ if (!ids.length) {
5252
+ await interruptible(Promise.all([...table.jobs.values()].map(job => job.promise)), this.signal);
5253
+ table.jobs.clear();
5254
+ return 0;
5255
+ }
5256
+ let status = invalid ? 127 : 0;
5257
+ for (const id of ids) {
5258
+ const job = table.resolve(id);
5259
+ if (!job) {
5260
+ status = 127;
5261
+ continue;
5262
+ }
5263
+ status = await interruptible(job.promise, this.signal);
5264
+ job.consumed = true;
5265
+ job.waited = true;
5266
+ await publish(job.id);
5267
+ }
5268
+ return status;
5269
+ }
5027
5270
  async builtin(context, state, assignments, diagnose, suppressSpecial = false) {
5028
5271
  const { command, args, stdout, stderr } = context;
5029
5272
  if (command === ":" || command === "true")
5030
5273
  return 0;
5031
5274
  if (command === "false")
5032
5275
  return 1;
5276
+ if (command === "wait")
5277
+ return this.waitBuiltin(context, state);
5033
5278
  if (command === "shopt")
5034
5279
  return this.shoptBuiltin(context, state);
5035
5280
  if (command === "let")
@@ -5829,11 +6074,14 @@ export class Runtime {
5829
6074
  const substitutionDiagnosticLines = new Map();
5830
6075
  for (const [command, line] of part.script.printedLines ?? [])
5831
6076
  substitutionDiagnosticLines.set(command, part.sourceLine === undefined ? warningLine + (command.line ?? part.line) - part.line : warningLine + line - 1);
5832
- const captureIO = { ...isolateIO(io), substitutionDiagnosticLines, diagnosticOffset: (io.diagnosticLine ?? part.line) - (part.sourceLine ?? part.line), stdout: this.budget.sink(capture, this.signal) };
6077
+ const resources = new BackgroundResources();
6078
+ const captureIO = { ...isolateIO(io), [backgroundResources]: [...io[backgroundResources] ?? [], resources], substitutionDiagnosticLines, diagnosticOffset: (io.diagnosticLine ?? part.line) - (part.sourceLine ?? part.line), stdout: this.budget.sink(capture, this.signal) };
6079
+ resources.destination = backgroundDestination(captureIO.stdout);
5833
6080
  try {
5834
6081
  state.substitutionStatus = fileShortcut ? await this.runCommandIsolated(command, child, captureIO, true) : await this.run(part.script, child, captureIO);
5835
6082
  }
5836
6083
  finally {
6084
+ await interruptible(resources.drain(), this.signal);
5837
6085
  stateMonitor(child)?.closeValues();
5838
6086
  }
5839
6087
  state.status = state.substitutionStatus;
@@ -5879,13 +6127,14 @@ export class Runtime {
5879
6127
  io[valueScope]?.reserve(values.length * 32 + 64, 0);
5880
6128
  return concatShellValues(values.flatMap((value, index) => index ? [separator, value] : [value]), io[valueScope]);
5881
6129
  }
5882
- let value = part.name === "?" ? String(state.status)
5883
- : part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}${state.braceexpand !== false ? "B" : ""}`
5884
- : part.name === "#" ? String(state.positional.length)
5885
- : part.name === "@" || part.name === "*" ? state.positional.join(hereString && (part.name === "@" || !part.quoted) ? " " : Array.from(state.variables.IFS ?? " ")[0] ?? "")
5886
- : /^0+$/u.test(part.name) ? state.arg0 ?? "virtual-bash"
5887
- : /^\d+$/u.test(part.name) ? state.positional[Number(part.name) - 1]
5888
- : this.variable(state, part.name);
6130
+ let value = part.name === "!" ? state.lastBackgroundJob
6131
+ : part.name === "?" ? String(state.status)
6132
+ : part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}${state.braceexpand !== false ? "B" : ""}`
6133
+ : part.name === "#" ? String(state.positional.length)
6134
+ : part.name === "@" || part.name === "*" ? state.positional.join(hereString && (part.name === "@" || !part.quoted) ? " " : Array.from(state.variables.IFS ?? " ")[0] ?? "")
6135
+ : /^0+$/u.test(part.name) ? state.arg0 ?? "virtual-bash"
6136
+ : /^\d+$/u.test(part.name) ? state.positional[Number(part.name) - 1]
6137
+ : this.variable(state, part.name);
5889
6138
  let retained = value;
5890
6139
  if (!part.length && value !== undefined) {
5891
6140
  if (/^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name) && !arrayStore(state)?.get(part.name))