@ai-ecoverse/slicc-kernel 1.0.0 → 1.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/README.md CHANGED
@@ -28,6 +28,7 @@ const { status, stdout, stderr } = await kernel.run(['bash', '-c', 'echo hi > he
28
28
  | `root` | `navigator.storage.getDirectory()` | the `FileSystemDirectoryHandle` that becomes `/` |
29
29
  | `modules` | `'/node_modules'` | where installed packages are scanned for commands |
30
30
  | `env` | `{}` | environment added to every run |
31
+ | `metadata` | `'slicc-kernel'` | the IndexedDB database for POSIX metadata (see [Metadata](#metadata)); `false` keeps it in memory for the kernel's lifetime |
31
32
  | `worker` | `new URL('./kernel-worker.js', import.meta.url)` | the kernel worker script |
32
33
 
33
34
  ### `kernel.run(argv, options?) → Promise<{ status, stdout, stderr }>`
@@ -37,10 +38,30 @@ Runs `argv[0]` with `argv` as its arguments and resolves when it exits. `stdout`
37
38
  | option | |
38
39
  | --- | --- |
39
40
  | `cwd` | working directory, default `/`; created in OPFS if missing |
40
- | `env` | extra environment; the defaults are `PATH=/usr/bin:/bin`, `HOME=/`, `PWD=<cwd>` |
41
+ | `env` | extra environment; the defaults are `PATH=/usr/bin:/bin`, `HOME=/home`, `PWD=<cwd>` |
41
42
  | `stdin` | a string or `Uint8Array`; without it stdin is `/dev/null` |
42
43
  | `onStdout`, `onStderr` | called with each chunk of text as it is written |
43
44
 
45
+ ### `kernel.openTerminal(argv, options?) → Promise<Terminal>`
46
+
47
+ Starts `argv` as the session leader on a new terminal and resolves once it is running, or rejects if `argv[0]` cannot be found. Its stdin, stdout and stderr are the terminal, which has a line discipline with echo, canonical mode and `ISIG`, and supports job control, so `kernel.openTerminal(['bash', '-i'])` gives an interactive shell. The API was designed with [slicc-spectrum](https://github.com/ai-ecoverse/slicc-spectrum), a terminal web component, and these names are kept stable.
48
+
49
+ | option | |
50
+ | --- | --- |
51
+ | `cwd`, `env` | as for `run`; `TERM=xterm-256color` and `COLORTERM=truecolor` are added |
52
+ | `cols`, `rows` | initial size, default 80 × 24, so `$COLUMNS` and `$LINES` are right before the first prompt |
53
+ | `onData` | called with each chunk of output as raw bytes (escape sequences included) |
54
+
55
+ | `Terminal` | |
56
+ | --- | --- |
57
+ | `pid` | the session leader's pid |
58
+ | `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
59
+ | `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group |
60
+ | `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
61
+ | `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
62
+ | `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
63
+ | `close()` | hangs up: `SIGHUP` to the foreground process group, and reads from the terminal return end-of-file |
64
+
44
65
  ### `kernel.terminate()`
45
66
 
46
67
  Stops the kernel worker and every process. Pending and later calls reject.
@@ -53,13 +74,32 @@ Commands come from installed packages in npm's `node_modules` layout: every `<mo
53
74
  { "slicc": { "abi": "emscripten", "commands": { "bash": { "glue": "bin/bash", "wasm": "bin/bash.wasm" } } } }
54
75
  ```
55
76
 
56
- Entries may also set `argv0`, `args` and `env` (with `${package}` expanded to the package directory), and a package-wide `slicc.env`. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, and a script through its `#!` line (including `#!/usr/bin/env name`). The catalog is re-read at the start of every `run`, so packages installed in between are picked up.
77
+ Entries may also set `argv0`, `args` and `env` (with `${package}` expanded to the package directory), and a package-wide `slicc.env`. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. The catalog is re-read at the start of every `run`, so packages installed in between are picked up.
57
78
 
58
79
  ## Filesystem
59
80
 
60
- `/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `/tmp`, `/dev`, `/proc` and anything created directly in `/` while a process runs live in that process's memory unless the directory already exists in OPFS. File contents are buffered per open file and written back on close, `fsync` and exit; metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
81
+ `/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. `/dev`, `/proc` and anything created directly in `/` while a process runs live in that process's memory unless it already exists in OPFS. File contents are buffered per open file and written back on close, `fsync` and exit; metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
82
+
83
+ Directories are renamed with `FileSystemHandle.move()` where available, else by copy and delete.
84
+
85
+ ### Metadata
61
86
 
62
- OPFS stores no POSIX metadata, so modes are synthesized: directories `0755`, files `0644`, and files under `node_modules/*/bin/` `0755`. `chmod` and `utime` are remembered for the lifetime of the kernel worker. Symlinks are not supported (`ENOSYS`). Directories are renamed with `FileSystemHandle.move()` where available, else by copy and delete.
87
+ OPFS stores names, bytes, sizes and modification times, nothing else. Everything POSIX needs on top of that lives in an IndexedDB sidecar, so it survives reloads and new kernels:
88
+
89
+ - mode bits (`chmod`);
90
+ - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again);
91
+ - inode numbers that stay stable across renames;
92
+ - symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar.
93
+
94
+ OPFS stays authoritative: an entry is consulted only for a path that exists in OPFS (symlinks only for paths that don't), so files written straight to OPFS, for example by the BIOS installer or a page, get the defaults: directories `0755`, files `0644`, and files under `node_modules/*/bin/` `0755`. A file deleted through the OPFS API disappears at once; its leftover entry is ignored and removed when the next kernel starts. Changes go to OPFS first and then to the sidecar, so a crash in between at worst loses the metadata of that one change. Each change is a read-modify-write inside one IndexedDB transaction, so two kernels (two tabs) on the same origin don't lose each other's updates.
95
+
96
+ The database is `slicc-kernel` (or the `metadata` option), schema version 1, with one object store, `entries`, keyed by absolute path (`keyPath: 'path'`) and an index `dir` on the parent directory of symlinks. A record looks like `{ path, mode?, atimeMs?, mtimeMs?, mtimeFor?, ctimeMs?, ino?, link?, dir? }`, where `mtimeFor` is the OPFS `lastModified` an explicit `mtimeMs` belongs to. A future schema bumps the version and migrates in `onupgradeneeded`; an older kernel then closes its connection. The sidecar is keyed by path from the `root` handle, so use a different `metadata` name for each distinct root.
97
+
98
+ Not covered: hard links (they fail with `EMLINK`, as Emscripten reports; OPFS cannot share contents between names) and file ownership. Emscripten's `chown` doesn't pass the owner to filesystem backends, so there is nothing to store, and processes see every file as owned by uid and gid 1000. Emscripten also doesn't enforce permission bits, so a file runs whether or not its exec bit is set; the bits are what `ls`, `stat` and `test` report.
99
+
100
+ Reads retry briefly when a concurrent write has invalidated the OPFS file snapshot (`NotReadableError`).
101
+
102
+ The filesystem is our own rather than [ZenFS](https://github.com/zen-fs/core) (which SLICC uses), because OPFS stays the single source of truth: other writers, such as the BIOS installing packages or a page writing files, need no index to stay consistent with, and nothing is preloaded into memory at mount. POSIX metadata lives in the IndexedDB sidecar described above, which has per-entry transactions instead of one JSON file rewritten on every change.
63
103
 
64
104
  ## What is in here
65
105
 
@@ -73,6 +113,10 @@ The kernel is ported from SLICC's `packages/webapp/src/kernel/` with the browser
73
113
 
74
114
  SLICC's networking (sockets, the TLS proxy) and WASI support are not included.
75
115
 
116
+ ## Installing from git
117
+
118
+ `npm install github:ai-ecoverse/slicc-kernel#<sha>` works: the `prepare` script builds `dist/`.
119
+
76
120
  ## Development
77
121
 
78
122
  ```sh
@@ -86,6 +130,8 @@ npm run test:unit
86
130
 
87
131
  Unit tests live in `test/unit/`, which stays out of git. They run real wasm bash and coreutils through the whole stack in Node `worker_threads` against an in-memory OPFS. The pre-commit hook runs them under coverage and requires every changed line in `src/` to be covered.
88
132
 
133
+ The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web), which also provides the `slicc-lint-comments` (no comments anywhere), `slicc-no-unit-tests` (no unit tests in git) and `slicc-diff-cover` (100% coverage of changed lines) commands that `npm run lint` and the pre-commit hook use.
134
+
89
135
  ## License
90
136
 
91
137
  Apache-2.0
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ async function createKernel(options = {}) {
20
20
  const call2 = pending.get(data.id);
21
21
  if (!call2) return;
22
22
  if (data.fd !== void 0) return call2.output?.(data.fd, data.bytes);
23
+ if (data.started !== void 0) return call2.started?.(data.started);
23
24
  pending.delete(data.id);
24
25
  if (data.error !== void 0) call2.reject(new Error(data.error));
25
26
  else call2.resolve(data.result);
@@ -28,19 +29,56 @@ async function createKernel(options = {}) {
28
29
  event.preventDefault();
29
30
  fail(new Error(`slicc-kernel worker failed: ${event.message}`));
30
31
  });
31
- function call(req, output) {
32
- if (failure) return Promise.reject(failure);
32
+ function request(req, handlers = {}) {
33
33
  const id = ++nextId;
34
- return new Promise((resolve, reject) => {
35
- pending.set(id, { resolve, reject, ...output ? { output } : {} });
34
+ if (failure) return { id, done: Promise.reject(failure) };
35
+ const done = new Promise((resolve, reject) => {
36
+ pending.set(id, { resolve, reject, ...handlers });
36
37
  worker.postMessage({ ...req, id });
37
38
  });
39
+ return { id, done };
40
+ }
41
+ const call = (req, handlers) => request(req, handlers).done;
42
+ async function openTerminal(argv, opts = {}) {
43
+ const { onData, ...options2 } = opts;
44
+ let listener = onData ?? null;
45
+ const backlog = [];
46
+ const started = Promise.withResolvers();
47
+ const { id, done } = request(
48
+ { op: "open-terminal", argv, options: options2 },
49
+ {
50
+ output: (_fd, bytes) => listener ? listener(bytes) : backlog.push(bytes),
51
+ started: started.resolve
52
+ }
53
+ );
54
+ done.catch(started.reject);
55
+ const pid = await started.promise;
56
+ const send = (action) => void call({ op: "terminal", terminal: id, ...action }).catch(() => void 0);
57
+ return {
58
+ pid,
59
+ exited: done,
60
+ get onData() {
61
+ return listener;
62
+ },
63
+ set onData(next) {
64
+ listener = next;
65
+ if (next) for (const bytes of backlog.splice(0)) next(bytes);
66
+ },
67
+ write: (data) => send({
68
+ action: "write",
69
+ bytes: typeof data === "string" ? new TextEncoder().encode(data) : data
70
+ }),
71
+ resize: (cols, rows) => send({ action: "resize", cols, rows }),
72
+ signal: (name) => send({ action: "signal", signal: name }),
73
+ close: () => send({ action: "close" })
74
+ };
38
75
  }
39
76
  await call({
40
77
  op: "init",
41
78
  ...options.root ? { root: options.root } : {},
42
79
  ...options.modules ? { modules: options.modules } : {},
43
- ...options.env ? { env: options.env } : {}
80
+ ...options.env ? { env: options.env } : {},
81
+ ...options.metadata !== void 0 ? { metadata: options.metadata } : {}
44
82
  });
45
83
  return {
46
84
  async run(argv, runOptions = {}) {
@@ -49,7 +87,7 @@ async function createKernel(options = {}) {
49
87
  const input = typeof stdin === "string" ? new TextEncoder().encode(stdin) : stdin;
50
88
  const result = await call(
51
89
  { op: "run", argv, options: { ...rest, ...input ? { stdin: input } : {} } },
52
- (fd, bytes) => streams[fd](bytes)
90
+ { output: (fd, bytes) => streams[fd](bytes) }
53
91
  );
54
92
  const decoder = new TextDecoder();
55
93
  return {
@@ -58,6 +96,7 @@ async function createKernel(options = {}) {
58
96
  stderr: decoder.decode(result.stderr)
59
97
  };
60
98
  },
99
+ openTerminal,
61
100
  terminate() {
62
101
  fail(new Error("slicc-kernel terminated"));
63
102
  worker.terminate();
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import type { KernelCall } from './serve.ts';\n\nexport interface KernelOptions {\n root?: FileSystemDirectoryHandle;\n modules?: string;\n env?: Record<string, string>;\n worker?: string | URL;\n}\n\nexport interface RunOptions {\n cwd?: string;\n env?: Record<string, string>;\n stdin?: string | Uint8Array;\n onStdout?: (text: string) => void;\n onStderr?: (text: string) => void;\n}\n\nexport interface RunResult {\n status: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface Kernel {\n run(argv: string[], options?: RunOptions): Promise<RunResult>;\n terminate(): void;\n}\n\ninterface Reply {\n id: number;\n result?: unknown;\n error?: string;\n fd?: 1 | 2;\n bytes?: Uint8Array;\n}\n\ninterface Pending {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n output?: (fd: 1 | 2, bytes: Uint8Array) => void;\n}\n\nconst ISOLATION =\n 'slicc-kernel needs a cross-origin isolated page (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp or credentialless)';\n\nfunction streamer(callback: ((text: string) => void) | undefined) {\n const decoder = new TextDecoder();\n return (bytes: Uint8Array) => callback?.(decoder.decode(bytes, { stream: true }));\n}\n\nexport async function createKernel(options: KernelOptions = {}): Promise<Kernel> {\n if (!globalThis.crossOriginIsolated) throw new Error(ISOLATION);\n const url = options.worker ?? new URL('./kernel-worker.js', import.meta.url);\n const worker = new Worker(url, { type: 'module', name: 'slicc-kernel' });\n const pending = new Map<number, Pending>();\n let nextId = 0;\n let failure: Error | undefined;\n\n const fail = (error: Error) => {\n failure = error;\n for (const call of pending.values()) call.reject(error);\n pending.clear();\n };\n worker.addEventListener('message', ({ data }: MessageEvent<Reply>) => {\n const call = pending.get(data.id);\n if (!call) return;\n if (data.fd !== undefined) return call.output?.(data.fd, data.bytes as Uint8Array);\n pending.delete(data.id);\n if (data.error !== undefined) call.reject(new Error(data.error));\n else call.resolve(data.result);\n });\n worker.addEventListener('error', (event) => {\n event.preventDefault();\n fail(new Error(`slicc-kernel worker failed: ${event.message}`));\n });\n\n function call(req: KernelCall, output?: Pending['output']): Promise<unknown> {\n if (failure) return Promise.reject(failure);\n const id = ++nextId;\n return new Promise((resolve, reject) => {\n pending.set(id, { resolve, reject, ...(output ? { output } : {}) });\n worker.postMessage({ ...req, id });\n });\n }\n\n await call({\n op: 'init',\n ...(options.root ? { root: options.root } : {}),\n ...(options.modules ? { modules: options.modules } : {}),\n ...(options.env ? { env: options.env } : {}),\n });\n\n return {\n async run(argv, runOptions = {}) {\n const { onStdout, onStderr, stdin, ...rest } = runOptions;\n const streams = { 1: streamer(onStdout), 2: streamer(onStderr) };\n const input = typeof stdin === 'string' ? new TextEncoder().encode(stdin) : stdin;\n const result = (await call(\n { op: 'run', argv, options: { ...rest, ...(input ? { stdin: input } : {}) } },\n (fd, bytes) => streams[fd](bytes)\n )) as { status: number; stdout: Uint8Array; stderr: Uint8Array };\n const decoder = new TextDecoder();\n return {\n status: result.status,\n stdout: decoder.decode(result.stdout),\n stderr: decoder.decode(result.stderr),\n };\n },\n terminate() {\n fail(new Error('slicc-kernel terminated'));\n worker.terminate();\n },\n };\n}\n"],
5
- "mappings": "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",
6
- "names": ["call"]
4
+ "sourcesContent": ["import type { KernelCall, TerminalAction } from './serve.ts';\n\nexport interface KernelOptions {\n root?: FileSystemDirectoryHandle;\n modules?: string;\n env?: Record<string, string>;\n metadata?: string | false;\n worker?: string | URL;\n}\n\nexport interface RunOptions {\n cwd?: string;\n env?: Record<string, string>;\n stdin?: string | Uint8Array;\n onStdout?: (text: string) => void;\n onStderr?: (text: string) => void;\n}\n\nexport interface RunResult {\n status: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface Kernel {\n run(argv: string[], options?: RunOptions): Promise<RunResult>;\n openTerminal(argv: string[], options?: TerminalOptions): Promise<Terminal>;\n terminate(): void;\n}\n\nexport interface TerminalOptions {\n cwd?: string;\n env?: Record<string, string>;\n cols?: number;\n rows?: number;\n onData?: (bytes: Uint8Array) => void;\n}\n\nexport type TerminalSignal =\n | 'SIGINT'\n | 'SIGTSTP'\n | 'SIGQUIT'\n | 'SIGHUP'\n | 'SIGTERM'\n | 'SIGKILL'\n | 'SIGCONT';\n\nexport interface Terminal {\n readonly pid: number;\n readonly exited: Promise<number>;\n onData: ((bytes: Uint8Array) => void) | null;\n write(data: string | Uint8Array): void;\n resize(cols: number, rows: number): void;\n signal(name: TerminalSignal): void;\n close(): void;\n}\n\ninterface Reply {\n id: number;\n result?: unknown;\n error?: string;\n fd?: 1 | 2;\n bytes?: Uint8Array;\n started?: number;\n}\n\ninterface Pending {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n output?: (fd: 1 | 2, bytes: Uint8Array) => void;\n started?: (pid: number) => void;\n}\n\ninterface Handlers {\n output?: Pending['output'];\n started?: Pending['started'];\n}\n\nconst ISOLATION =\n 'slicc-kernel needs a cross-origin isolated page (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp or credentialless)';\n\nfunction streamer(callback: ((text: string) => void) | undefined) {\n const decoder = new TextDecoder();\n return (bytes: Uint8Array) => callback?.(decoder.decode(bytes, { stream: true }));\n}\n\nexport async function createKernel(options: KernelOptions = {}): Promise<Kernel> {\n if (!globalThis.crossOriginIsolated) throw new Error(ISOLATION);\n const url = options.worker ?? new URL('./kernel-worker.js', import.meta.url);\n const worker = new Worker(url, { type: 'module', name: 'slicc-kernel' });\n const pending = new Map<number, Pending>();\n let nextId = 0;\n let failure: Error | undefined;\n\n const fail = (error: Error) => {\n failure = error;\n for (const call of pending.values()) call.reject(error);\n pending.clear();\n };\n worker.addEventListener('message', ({ data }: MessageEvent<Reply>) => {\n const call = pending.get(data.id);\n if (!call) return;\n if (data.fd !== undefined) return call.output?.(data.fd, data.bytes as Uint8Array);\n if (data.started !== undefined) return call.started?.(data.started);\n pending.delete(data.id);\n if (data.error !== undefined) call.reject(new Error(data.error));\n else call.resolve(data.result);\n });\n worker.addEventListener('error', (event) => {\n event.preventDefault();\n fail(new Error(`slicc-kernel worker failed: ${event.message}`));\n });\n\n function request(\n req: KernelCall,\n handlers: Handlers = {}\n ): { id: number; done: Promise<unknown> } {\n const id = ++nextId;\n if (failure) return { id, done: Promise.reject(failure) };\n const done = new Promise((resolve, reject) => {\n pending.set(id, { resolve, reject, ...handlers });\n worker.postMessage({ ...req, id });\n });\n return { id, done };\n }\n\n const call = (req: KernelCall, handlers?: Handlers) => request(req, handlers).done;\n\n async function openTerminal(argv: string[], opts: TerminalOptions = {}): Promise<Terminal> {\n const { onData, ...options } = opts;\n let listener = onData ?? null;\n const backlog: Uint8Array[] = [];\n const started = Promise.withResolvers<number>();\n const { id, done } = request(\n { op: 'open-terminal', argv, options },\n {\n output: (_fd, bytes) => (listener ? listener(bytes) : backlog.push(bytes)),\n started: started.resolve,\n }\n );\n done.catch(started.reject);\n const pid = await started.promise;\n const send = (action: TerminalAction) =>\n void call({ op: 'terminal', terminal: id, ...action }).catch(() => undefined);\n return {\n pid,\n exited: done as Promise<number>,\n get onData() {\n return listener;\n },\n set onData(next) {\n listener = next;\n if (next) for (const bytes of backlog.splice(0)) next(bytes);\n },\n write: (data) =>\n send({\n action: 'write',\n bytes: typeof data === 'string' ? new TextEncoder().encode(data) : data,\n }),\n resize: (cols, rows) => send({ action: 'resize', cols, rows }),\n signal: (name) => send({ action: 'signal', signal: name }),\n close: () => send({ action: 'close' }),\n };\n }\n\n await call({\n op: 'init',\n ...(options.root ? { root: options.root } : {}),\n ...(options.modules ? { modules: options.modules } : {}),\n ...(options.env ? { env: options.env } : {}),\n ...(options.metadata !== undefined ? { metadata: options.metadata } : {}),\n });\n\n return {\n async run(argv, runOptions = {}) {\n const { onStdout, onStderr, stdin, ...rest } = runOptions;\n const streams = { 1: streamer(onStdout), 2: streamer(onStderr) };\n const input = typeof stdin === 'string' ? new TextEncoder().encode(stdin) : stdin;\n const result = (await call(\n { op: 'run', argv, options: { ...rest, ...(input ? { stdin: input } : {}) } },\n { output: (fd, bytes) => streams[fd](bytes) }\n )) as { status: number; stdout: Uint8Array; stderr: Uint8Array };\n const decoder = new TextDecoder();\n return {\n status: result.status,\n stdout: decoder.decode(result.stdout),\n stderr: decoder.decode(result.stderr),\n };\n },\n openTerminal,\n terminate() {\n fail(new Error('slicc-kernel terminated'));\n worker.terminate();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["call", "options"]
7
7
  }