@nimbus-sh/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/runtime/bash-runner.d.ts +63 -0
- package/dist/runtime/bash-runner.d.ts.map +1 -0
- package/dist/runtime/bash-runner.generated.d.ts +14 -0
- package/dist/runtime/bash-runner.generated.d.ts.map +1 -0
- package/dist/runtime/bash-runner.generated.js +13 -0
- package/dist/runtime/bash-runner.js +290 -0
- package/dist/runtime/cpython-runner.d.ts +86 -0
- package/dist/runtime/cpython-runner.d.ts.map +1 -0
- package/dist/runtime/cpython-runner.js +425 -0
- package/dist/runtime/facet-host.d.ts +159 -0
- package/dist/runtime/facet-host.d.ts.map +1 -0
- package/dist/runtime/facet-host.js +22 -0
- package/dist/runtime/installed-runtimes.d.ts +99 -0
- package/dist/runtime/installed-runtimes.d.ts.map +1 -0
- package/dist/runtime/installed-runtimes.js +162 -0
- package/dist/runtime/local-facet-host.d.ts +38 -0
- package/dist/runtime/local-facet-host.d.ts.map +1 -0
- package/dist/runtime/local-facet-host.js +171 -0
- package/dist/runtime/python-pip.d.ts +38 -0
- package/dist/runtime/python-pip.d.ts.map +1 -0
- package/dist/runtime/python-pip.js +1063 -0
- package/dist/runtime/runtime-manifest.d.ts +86 -0
- package/dist/runtime/runtime-manifest.d.ts.map +1 -0
- package/dist/runtime/runtime-manifest.js +72 -0
- package/dist/runtime/runtime-registry.d.ts +161 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -0
- package/dist/runtime/runtime-registry.js +363 -0
- package/dist/runtime/vfs-snapshot.d.ts.map +1 -1
- package/dist/runtime/vfs-snapshot.js +15 -1
- package/dist/runtime/vfs-supervisor.d.ts +22 -0
- package/dist/runtime/vfs-supervisor.d.ts.map +1 -0
- package/dist/runtime/vfs-supervisor.js +65 -0
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +14 -0
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -0
- package/dist/runtime/virtual-socket-kernel.generated.js +13 -0
- package/dist/runtime/wasm-runner.d.ts +80 -0
- package/dist/runtime/wasm-runner.d.ts.map +1 -0
- package/dist/runtime/wasm-runner.js +686 -0
- package/dist/workspace/nimbus-workspace.d.ts +16 -0
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +57 -2
- package/package.json +4 -2
- package/src/index.ts +9 -0
- package/src/runtime/bash-runner.generated.ts +14 -0
- package/src/runtime/bash-runner.ts +347 -0
- package/src/runtime/cpython-runner.ts +504 -0
- package/src/runtime/facet-host.ts +170 -0
- package/src/runtime/installed-runtimes.ts +235 -0
- package/src/runtime/local-facet-host.ts +205 -0
- package/src/runtime/python-pip.ts +1211 -0
- package/src/runtime/runtime-manifest.ts +155 -0
- package/src/runtime/runtime-registry.ts +510 -0
- package/src/runtime/vfs-snapshot.ts +15 -1
- package/src/runtime/vfs-supervisor.ts +67 -0
- package/src/runtime/virtual-socket-kernel.generated.ts +14 -0
- package/src/runtime/wasm-runner.ts +835 -0
- package/src/workspace/nimbus-workspace.ts +102 -3
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash-runner — real GNU bash 5.2.37 (wasm32-wasi, asyncified) in a
|
|
3
|
+
* dedicated facet.
|
|
4
|
+
*
|
|
5
|
+
* bash CANNOT run through the stock JSPI wasm-runner: the binary is
|
|
6
|
+
* asyncify-instrumented (fork/setjmp/blocking-pipe unwinds) and needs
|
|
7
|
+
* 15 `nimbus_proc` imports plus MULTIPLE instances per facet (fork).
|
|
8
|
+
* This runner embeds the proven multi-instance fork/pipe/exec/setjmp
|
|
9
|
+
* scheduler (packages/worker/wasm/bash/run-bash-fork.mjs — the local
|
|
10
|
+
* acid-test driver, itself a port of the PROVEN-LIVE fork M1/M2/M3
|
|
11
|
+
* mechanisms) as a facet preamble.
|
|
12
|
+
*
|
|
13
|
+
* Architecture (mirrors ruby-runner's facet dispatch):
|
|
14
|
+
* - bash.async.wasm + the coreutil exec targets ship through the facet
|
|
15
|
+
* host, which compiles them and exposes them on
|
|
16
|
+
* globalThis.__NIMBUS_WASM.
|
|
17
|
+
* - The preamble defines __bashBoot / __bashFeed. Boot instantiates
|
|
18
|
+
* bash, pumps the scheduler until the process tree exits or the
|
|
19
|
+
* root parks on a terminal stdin read; each feed delivers stdin
|
|
20
|
+
* bytes and pumps again. Facet state persists across submits on
|
|
21
|
+
* the warm isolate (same mechanism as __rubyInstance caching).
|
|
22
|
+
* - stdout/stderr accumulate per pump slice and stream back to the
|
|
23
|
+
* CommandContext; VFS writes come back as a WasiFsDiff on exit.
|
|
24
|
+
*/
|
|
25
|
+
import type { RuntimeManifest } from './runtime-manifest.js';
|
|
26
|
+
import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
|
|
27
|
+
import type { Facet, FacetHost } from './facet-host.js';
|
|
28
|
+
import type { Command, CommandContext, CommandInputStream } from '../substrate/lifo/commands/types.js';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
import type { WasiFsDiff } from './vfs-snapshot.js';
|
|
31
|
+
import type { BashBootArgs, BashFeedArgs, BashSlice } from './bash/types.js';
|
|
32
|
+
import { BASH_RUNNER_BODY_SRC } from './bash-runner.generated.js';
|
|
33
|
+
import { flushVfsDiff, snapshotVfs } from './vfs-snapshot.js';
|
|
34
|
+
import { requireVfsCred } from './os-contracts.js';
|
|
35
|
+
import { resolveVfsPath } from '../vfs/path.js';
|
|
36
|
+
|
|
37
|
+
type BashRunnerFactory = (
|
|
38
|
+
manifest: RuntimeManifest,
|
|
39
|
+
installRoot: string,
|
|
40
|
+
binName: string,
|
|
41
|
+
binKind: string | undefined,
|
|
42
|
+
) => Command;
|
|
43
|
+
|
|
44
|
+
type BashStepArgs = BashBootArgs | BashFeedArgs;
|
|
45
|
+
|
|
46
|
+
const BashSliceSchema = z.object({
|
|
47
|
+
state: z.enum(['need-input', 'exited', 'error']),
|
|
48
|
+
exitCode: z.number().optional(),
|
|
49
|
+
stdout: z.string().optional(),
|
|
50
|
+
stderr: z.string().optional(),
|
|
51
|
+
error: z.string().optional(),
|
|
52
|
+
fsDiff: z.custom<WasiFsDiff>().optional(),
|
|
53
|
+
stats: z.record(z.string(), z.unknown()).optional(),
|
|
54
|
+
}).passthrough();
|
|
55
|
+
|
|
56
|
+
function normalizeSlice(raw: unknown): BashSlice | null {
|
|
57
|
+
const parsed = BashSliceSchema.safeParse(raw);
|
|
58
|
+
if (!parsed.success) return null;
|
|
59
|
+
return {
|
|
60
|
+
state: parsed.data.state,
|
|
61
|
+
exitCode: Number(parsed.data.exitCode ?? 0),
|
|
62
|
+
stdout: parsed.data.stdout || '',
|
|
63
|
+
stderr: parsed.data.stderr || '',
|
|
64
|
+
error: parsed.data.error,
|
|
65
|
+
fsDiff: parsed.data.fsDiff,
|
|
66
|
+
stats: parsed.data.stats,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
71
|
+
const out = new ArrayBuffer(bytes.byteLength);
|
|
72
|
+
new Uint8Array(out).set(bytes);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function errorMessage(error: unknown): string {
|
|
77
|
+
if (error instanceof Error) return error.message;
|
|
78
|
+
return String(error);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function bashFacetStep(args: BashStepArgs): Promise<unknown> {
|
|
82
|
+
const boot = Reflect.get(globalThis, '__bashBoot');
|
|
83
|
+
const feed = Reflect.get(globalThis, '__bashFeed');
|
|
84
|
+
if (typeof boot !== 'function' || typeof feed !== 'function') {
|
|
85
|
+
return {
|
|
86
|
+
state: 'error',
|
|
87
|
+
exitCode: 127,
|
|
88
|
+
stdout: '',
|
|
89
|
+
stderr: '',
|
|
90
|
+
error: 'bash-runner preamble missing (__bashBoot/__bashFeed not in scope)',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return args.op === 'boot' ? boot(args) : feed(args);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface BashFacetSession {
|
|
97
|
+
readonly initial: BashSlice;
|
|
98
|
+
push(data: string, eof?: boolean): Promise<BashSlice>;
|
|
99
|
+
close(): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function createBashFacetSession(deps: {
|
|
103
|
+
facets: FacetHost;
|
|
104
|
+
vfs: CredentialedVfs;
|
|
105
|
+
manifest: RuntimeManifest;
|
|
106
|
+
installRoot: string;
|
|
107
|
+
argv: string[];
|
|
108
|
+
env: Record<string, string>;
|
|
109
|
+
cwd: string;
|
|
110
|
+
stdinData?: string;
|
|
111
|
+
stdinClosed: boolean;
|
|
112
|
+
stdinTty: boolean;
|
|
113
|
+
extraRoots?: string[];
|
|
114
|
+
}): Promise<BashFacetSession> {
|
|
115
|
+
const findFile = (relativePath: string): string | null => {
|
|
116
|
+
const entry = deps.manifest.files.find((file) => file.path === relativePath);
|
|
117
|
+
return entry ? `${deps.installRoot}/${entry.path}` : null;
|
|
118
|
+
};
|
|
119
|
+
const bashWasmPath = findFile('share/bash/bash.async.wasm');
|
|
120
|
+
if (!bashWasmPath || !deps.vfs.exists(bashWasmPath)) {
|
|
121
|
+
throw new Error("bash.async.wasm missing (re-run 'nimbus install bash')");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const userEnv: Record<string, string> = { ...deps.env };
|
|
125
|
+
userEnv.HOME ||= '/home/user';
|
|
126
|
+
userEnv.PATH ||= '/bin:/usr/bin';
|
|
127
|
+
userEnv.TERM ||= 'dumb';
|
|
128
|
+
userEnv.NIMBUS_PWD = deps.cwd;
|
|
129
|
+
userEnv.BASH_ENV ||= '/etc/nimbus.bashrc';
|
|
130
|
+
userEnv.PWD = deps.cwd;
|
|
131
|
+
|
|
132
|
+
const extraRoots = [...(deps.extraRoots ?? [])];
|
|
133
|
+
if (userEnv.HOME !== '/home/user') extraRoots.push(userEnv.HOME);
|
|
134
|
+
const fsSnapshot = snapshotVfs(deps.vfs, deps.cwd, { extraRoots });
|
|
135
|
+
if ('error' in fsSnapshot) throw new Error(fsSnapshot.error);
|
|
136
|
+
|
|
137
|
+
const wasmModules: Record<string, ArrayBuffer> = {
|
|
138
|
+
'bash.async.wasm': toArrayBuffer(deps.vfs.readFile(bashWasmPath)),
|
|
139
|
+
};
|
|
140
|
+
for (const file of deps.manifest.files) {
|
|
141
|
+
const prefix = 'share/bash/coreutils/';
|
|
142
|
+
if (!file.path.startsWith(prefix) || !file.path.endsWith('.wasm')) continue;
|
|
143
|
+
const name = file.path.slice(prefix.length, -'.wasm'.length);
|
|
144
|
+
const vfsPath = `${deps.installRoot}/${file.path}`;
|
|
145
|
+
if (deps.vfs.exists(vfsPath)) {
|
|
146
|
+
wasmModules[`cu_${name}.wasm`] = toArrayBuffer(deps.vfs.readFile(vfsPath));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const appletsPath = findFile('share/bash/coreutils/busybox.applets');
|
|
151
|
+
const busyboxApplets = appletsPath && deps.vfs.exists(appletsPath)
|
|
152
|
+
? new TextDecoder().decode(deps.vfs.readFile(appletsPath))
|
|
153
|
+
.split('\n')
|
|
154
|
+
.map((line) => line.trim())
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
: [];
|
|
157
|
+
|
|
158
|
+
const facet: Facet = deps.facets.open({
|
|
159
|
+
tag: 'bash-runner',
|
|
160
|
+
concurrency: 1,
|
|
161
|
+
// No supervisor capability: bash is seeded with its subtree by value and
|
|
162
|
+
// hands the whole mutation back as a diff on exit, so it makes no syscall
|
|
163
|
+
// into the session while it runs.
|
|
164
|
+
preamble: BASH_RUNNER_PREAMBLE,
|
|
165
|
+
wasmModules,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
let active = true;
|
|
169
|
+
let closed = false;
|
|
170
|
+
const submit = async (args: BashStepArgs): Promise<BashSlice> => {
|
|
171
|
+
const slice = normalizeSlice(
|
|
172
|
+
await facet.submit<BashStepArgs, unknown>(bashFacetStep, args, { timeoutMs: 300_000 }),
|
|
173
|
+
);
|
|
174
|
+
if (!slice) throw new Error('facet returned an invalid payload');
|
|
175
|
+
if (slice.state === 'exited') {
|
|
176
|
+
if (slice.fsDiff) flushVfsDiff(deps.vfs, slice.fsDiff);
|
|
177
|
+
active = false;
|
|
178
|
+
} else if (slice.state === 'error') {
|
|
179
|
+
active = false;
|
|
180
|
+
}
|
|
181
|
+
return slice;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const initial = await submit({
|
|
186
|
+
op: 'boot',
|
|
187
|
+
argv: deps.argv,
|
|
188
|
+
environ: Object.entries(userEnv).map(([key, value]) => `${key}=${value}`),
|
|
189
|
+
cwd: deps.cwd,
|
|
190
|
+
fsSnapshot: fsSnapshot.snapshot,
|
|
191
|
+
stdinData: deps.stdinData ?? '',
|
|
192
|
+
stdinClosed: deps.stdinClosed,
|
|
193
|
+
stdinTty: deps.stdinTty,
|
|
194
|
+
busyboxApplets,
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
initial,
|
|
198
|
+
push(data, eof = false) {
|
|
199
|
+
if (closed) throw new Error('bash facet session is closed');
|
|
200
|
+
return submit({ op: 'feed', data, eof });
|
|
201
|
+
},
|
|
202
|
+
async close() {
|
|
203
|
+
if (closed) return;
|
|
204
|
+
try {
|
|
205
|
+
if (active) await submit({ op: 'feed', data: '', eof: true });
|
|
206
|
+
} catch {
|
|
207
|
+
// Session teardown is best-effort; the owning command already
|
|
208
|
+
// reports dispatch failures from boot/push.
|
|
209
|
+
} finally {
|
|
210
|
+
closed = true;
|
|
211
|
+
facet.dispose();
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
} catch (error: unknown) {
|
|
216
|
+
facet.dispose();
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** bash flags that consume the following argv element. */
|
|
222
|
+
const BASH_OPT_WITH_ARG = new Set(['-c', '-o', '+o', '--rcfile', '--init-file']);
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Locate the script-path argv element (first non-flag arg when -c is
|
|
226
|
+
* absent) so the handler can resolve it against the session cwd —
|
|
227
|
+
* bash's own cwd inside the facet starts at '/' until the BASH_ENV
|
|
228
|
+
* chdir runs, so relative script paths must be made absolute host-side.
|
|
229
|
+
* Returns the argv index or -1 (interactive / -c / stdin modes).
|
|
230
|
+
*/
|
|
231
|
+
function findScriptArgIndex(argv: string[]): number {
|
|
232
|
+
for (let i = 0; i < argv.length; i++) {
|
|
233
|
+
const a = argv[i];
|
|
234
|
+
if (a === '--') return i + 1 < argv.length ? i + 1 : -1;
|
|
235
|
+
if (a === '-' ) return -1; // read from stdin
|
|
236
|
+
if (a.startsWith('-') || a.startsWith('+')) {
|
|
237
|
+
if (BASH_OPT_WITH_ARG.has(a)) {
|
|
238
|
+
if (a === '-c') return -1; // command string mode
|
|
239
|
+
i++; // skip the option's argument
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
return i;
|
|
244
|
+
}
|
|
245
|
+
return -1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function makeBashRunnerFactory(deps: {
|
|
249
|
+
facets: FacetHost;
|
|
250
|
+
vfs: SqliteVFS;
|
|
251
|
+
}): BashRunnerFactory {
|
|
252
|
+
return function bashRunnerFactory(manifest, installRoot, binName, _binKind) {
|
|
253
|
+
return async function bashBinHandler(ctx: CommandContext): Promise<number> {
|
|
254
|
+
// All VFS access (runtime wasm reads, script probes, snapshot,
|
|
255
|
+
// fsDiff writeback) runs as the INVOKING process credential —
|
|
256
|
+
// S2a enforcement applies to bash exactly as to ruby/python.
|
|
257
|
+
const cred = requireVfsCred('cred' in ctx ? ctx.cred : undefined, binName);
|
|
258
|
+
const vfs = deps.vfs.as(cred);
|
|
259
|
+
const argv = [...(ctx.args ?? [])];
|
|
260
|
+
const cwd = ctx.cwd || '/home/user';
|
|
261
|
+
|
|
262
|
+
// Resolve a relative script path against the session cwd.
|
|
263
|
+
const scriptIdx = findScriptArgIndex(argv);
|
|
264
|
+
const extraRoots: string[] = [];
|
|
265
|
+
if (scriptIdx >= 0) {
|
|
266
|
+
const abs = resolveVfsPath(argv[scriptIdx], cwd);
|
|
267
|
+
if (!vfs.exists(abs)) {
|
|
268
|
+
ctx.stderr.write(`${binName}: ${argv[scriptIdx]}: No such file or directory\n`);
|
|
269
|
+
return 127;
|
|
270
|
+
}
|
|
271
|
+
// Pass bash an ABSOLUTE path: the facet chdir's to the session
|
|
272
|
+
// cwd via BASH_ENV before opening the script, so a relative arg
|
|
273
|
+
// would resolve against cwd twice. resolveVfsPath returns a
|
|
274
|
+
// slash-less canonical key; re-anchor it at root.
|
|
275
|
+
argv[scriptIdx] = '/' + abs;
|
|
276
|
+
const dir = abs.replace(/\/[^/]*$/, '');
|
|
277
|
+
if (dir) extraRoots.push(dir);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// stdin plumbing. A terminal-backed fd 0 feeds incrementally
|
|
281
|
+
// (interactive bash, `read` builtins); a piped stdin is drained
|
|
282
|
+
// upfront and closed so the scheduler never parks on it.
|
|
283
|
+
const stdinIsTty = typeof ctx.isFdTerminal === 'function' ? ctx.isFdTerminal(0) : !ctx.stdin;
|
|
284
|
+
const feedStream: CommandInputStream | undefined = ctx.terminalStdin ?? ctx.stdin;
|
|
285
|
+
let stdinData = '';
|
|
286
|
+
let stdinClosed = true;
|
|
287
|
+
if (stdinIsTty && feedStream) {
|
|
288
|
+
stdinClosed = false;
|
|
289
|
+
} else if (ctx.stdin) {
|
|
290
|
+
stdinData = await ctx.stdin.readAll();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let session: BashFacetSession | null = null;
|
|
294
|
+
try {
|
|
295
|
+
session = await createBashFacetSession({
|
|
296
|
+
facets: deps.facets,
|
|
297
|
+
vfs,
|
|
298
|
+
manifest,
|
|
299
|
+
installRoot,
|
|
300
|
+
argv: [binName, ...argv],
|
|
301
|
+
env: ctx.env || {},
|
|
302
|
+
cwd,
|
|
303
|
+
stdinData,
|
|
304
|
+
stdinClosed,
|
|
305
|
+
stdinTty: stdinIsTty,
|
|
306
|
+
extraRoots,
|
|
307
|
+
});
|
|
308
|
+
let slice = session.initial;
|
|
309
|
+
|
|
310
|
+
for (;;) {
|
|
311
|
+
if (slice.stdout) ctx.stdout.write(slice.stdout);
|
|
312
|
+
if (slice.stderr) ctx.stderr.write(slice.stderr);
|
|
313
|
+
if (slice.state === 'exited') {
|
|
314
|
+
return slice.exitCode;
|
|
315
|
+
}
|
|
316
|
+
if (slice.state === 'error') {
|
|
317
|
+
ctx.stderr.write(`${binName}: ${slice.error || 'bash facet error'}\n`);
|
|
318
|
+
return slice.exitCode || 1;
|
|
319
|
+
}
|
|
320
|
+
// need-input: pull the next chunk from the terminal.
|
|
321
|
+
let data = '';
|
|
322
|
+
let eof = true;
|
|
323
|
+
if (!ctx.signal.aborted && feedStream) {
|
|
324
|
+
const chunk = await feedStream.read();
|
|
325
|
+
if (!ctx.signal.aborted) { data = chunk === null ? '' : chunk.replace(/\r\n?/g, '\n'); eof = chunk === null; }
|
|
326
|
+
}
|
|
327
|
+
slice = await session.push(data, eof);
|
|
328
|
+
}
|
|
329
|
+
} catch (e: unknown) {
|
|
330
|
+
ctx.stderr.write(`${binName}: dispatch failed: ${errorMessage(e)}\n`);
|
|
331
|
+
return 1;
|
|
332
|
+
} finally {
|
|
333
|
+
await session?.close();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Source string injected as the facet `preamble`. The facet's scope evaluates
|
|
341
|
+
* it verbatim so `__bashBoot` / `__bashFeed` are in scope when the user fn
|
|
342
|
+
* runs. Self-contained — no closure captures, no imports.
|
|
343
|
+
*
|
|
344
|
+
* The scheduler itself lives in `bash/preamble.ts` as real TypeScript; the build
|
|
345
|
+
* bundles it into `bash-runner.generated.ts`.
|
|
346
|
+
*/
|
|
347
|
+
export const BASH_RUNNER_PREAMBLE: string = BASH_RUNNER_BODY_SRC;
|