Download packages/experimental/webworker-runtime/src/shell/interpret.ts from SaylorTwift/deepseek-harness: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/SaylorTwift/deepseek-harness/resolve/main/packages/experimental/webworker-runtime/src/shell/interpret.ts
- Command line
-
hf download hf://SaylorTwift/deepseek-harness/packages/experimental/webworker-runtime/src/shell/interpret.ts
-
curl -L -o interpret.ts https://huggingface.co/SaylorTwift/deepseek-harness/resolve/main/packages/experimental/webworker-runtime/src/shell/interpret.ts
16 kB
| /** | |
| * The interpreter: it walks the parsed command line and runs the command table | |
| * against the VFS. Structure (`;` `&` `|` `|&` `&&` `||`, subshells, groups, | |
| * redirections, prefix assignments) is honored here; what a command *does* | |
| * belongs to its program in `./programs/`. | |
| * | |
| * Output is text, not streams: every program is a JavaScript function that | |
| * returns before the next one runs, so a pipeline hands a string along instead | |
| * of plumbing byte streams a browser worker has no way to schedule between. | |
| * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/shell/interpret | |
| */ | |
| import { parseShell } from '@yarnpkg/parsers' | |
| import type { Command, CommandChain, CommandLine, RedirectArgument, ShellLine, ValueArgument } from './ast.ts' | |
| import { expandArgument, isGlobPattern } from './expand.ts' | |
| import type { ExpansionContext } from './expand.ts' | |
| import { describeFailure, hostFileSystem, resolveIn } from './fs-access.ts' | |
| import { standardPrograms } from './programs/index.ts' | |
| import type { ShellFileSystem, ShellIo, ShellProgram, ShellRunOutcome, ShellState } from './types.ts' | |
| /** Status a command line reports once the caller's abort signal has fired. */ | |
| const ABORTED_STATUS = 130 | |
| /** Status of a command name the table does not hold, as POSIX shells report it. */ | |
| const NOT_FOUND_STATUS = 127 | |
| /** Nesting limit for `$( … )`; a deeper line is a runaway, not a command. */ | |
| const MAX_SUBSTITUTION_DEPTH = 16 | |
| /** Everything one `bash -c` invocation needs. */ | |
| export interface ShellRunOptions { | |
| /** Working directory the line starts in. */ | |
| cwd: string | |
| /** Environment the line starts with. */ | |
| env: Record<string, string> | |
| /** Standard input contents; absent means empty. */ | |
| stdin?: string | undefined | |
| /** Cancellation: an aborted line stops before its next command. */ | |
| signal?: AbortSignal | undefined | |
| /** | |
| * The filesystem this run acts on; defaults to the VFS mounted in this | |
| * thread. A run inside a process worker passes the message-backed one. | |
| */ | |
| fs?: ShellFileSystem | undefined | |
| /** | |
| * Called with each write as it happens, before the run settles. The returned | |
| * outcome still carries the complete text; this only lets a caller that | |
| * reports progress (a background job's incremental reads) see output while | |
| * the line is still running. | |
| */ | |
| onOutput?: ((stream: 'stdout' | 'stderr', text: string) => void) | undefined | |
| } | |
| /** Accumulates one output stream. */ | |
| interface Sink { | |
| write: (text: string) => void | |
| } | |
| /** A sink over a string buffer, for pipelines and command substitution. */ | |
| function buffer(): Sink & { text(): string } { | |
| const chunks: string[] = [] | |
| return { | |
| write: (text: string) => { chunks.push(text) }, | |
| text: () => chunks.join(''), | |
| } | |
| } | |
| /** | |
| * Run one shell command line to completion. | |
| * @param source - the command source, exactly as `bash -c` would receive it. | |
| * @param options - starting directory, environment, standard input, cancellation, filesystem, output callback. | |
| * @returns the exit status and the complete standard output and standard error. | |
| */ | |
| export async function runShellCommand(source: string, options: ShellRunOptions): Promise<ShellRunOutcome> { | |
| const run = startRun(options) | |
| let line: ShellLine | |
| try { | |
| line = parseShell(source, { isGlobPattern }) | |
| } catch (error) { | |
| run.io.err(`bash: syntax error: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}\n`) | |
| return run.settle(2) | |
| } | |
| const interpreter = new Interpreter(standardPrograms(), options.fs ?? hostFileSystem(), options.signal) | |
| return run.settle(await interpreter.line(line, run.state, run.io)) | |
| } | |
| /** | |
| * Run one program directly, without a command line to parse. | |
| * | |
| * This is the path for an argv the caller already has in pieces — a spawn that | |
| * names a program instead of handing `bash` a script — so nothing re-quotes | |
| * words that were never quoted in the first place. | |
| * @param argv - the program name at index 0, then its arguments. | |
| * @param options - starting directory, environment, standard input, cancellation, filesystem, output callback. | |
| * @returns the exit status and the complete standard output and standard error. | |
| */ | |
| export async function runShellProgram(argv: readonly string[], options: ShellRunOptions): Promise<ShellRunOutcome> { | |
| const run = startRun(options) | |
| const name = argv[0] | |
| const program = name === undefined ? undefined : standardPrograms().get(name) | |
| if (name === undefined || program === undefined) { | |
| run.io.err(`bash: ${name ?? ''}: command not found\n`) | |
| return run.settle(NOT_FOUND_STATUS) | |
| } | |
| if (options.signal?.aborted === true) return run.settle(ABORTED_STATUS) | |
| try { | |
| return run.settle(await program(argv, run.io, run.state, options.fs ?? hostFileSystem())) | |
| } catch (error) { | |
| run.io.err(`bash: ${name}: ${error instanceof Error ? error.message : String(error)}\n`) | |
| return run.settle(1) | |
| } | |
| } | |
| /** Build the state, the sinks, and the settlement one run reports through. */ | |
| function startRun(options: ShellRunOptions): { | |
| state: ShellState | |
| io: ShellIo | |
| settle: (exitCode: number) => ShellRunOutcome | |
| } { | |
| const stdout = buffer() | |
| const stderr = buffer() | |
| const report = options.onOutput | |
| return { | |
| state: { | |
| cwd: options.cwd, | |
| environment: { ...options.env }, | |
| variables: {}, | |
| lastStatus: 0, | |
| exitRequested: undefined, | |
| signal: options.signal, | |
| }, | |
| io: { | |
| stdin: options.stdin ?? '', | |
| out: (text: string) => { | |
| stdout.write(text) | |
| report?.('stdout', text) | |
| }, | |
| err: (text: string) => { | |
| stderr.write(text) | |
| report?.('stderr', text) | |
| }, | |
| }, | |
| settle: (exitCode: number) => ({ exitCode, stdout: stdout.text(), stderr: stderr.text() }), | |
| } | |
| } | |
| /** One interpretation pass; holds what every nested command shares. */ | |
| class Interpreter { | |
| constructor( | |
| private readonly programs: ReadonlyMap<string, ShellProgram>, | |
| private readonly fs: ShellFileSystem, | |
| private readonly signal: AbortSignal | undefined, | |
| private readonly depth = 0, | |
| ) {} | |
| /** | |
| * Run every command of one line, left to right. | |
| * @param line - the parsed line. | |
| * @param state - shell state the line reads and mutates. | |
| * @param io - standard input and the output sinks. | |
| * @returns the status of the last command that ran. | |
| */ | |
| async line(line: ShellLine, state: ShellState, io: ShellIo): Promise<number> { | |
| let status = state.lastStatus | |
| for (const entry of line) { | |
| if (this.signal?.aborted === true) return ABORTED_STATUS | |
| // `&` starts no background job here: the worker has no scheduler that | |
| // could run one, so a backgrounded command runs to completion in place. | |
| status = await this.commandLine(entry.command, state, io) | |
| state.lastStatus = status | |
| if (state.exitRequested !== undefined) return state.exitRequested | |
| } | |
| return status | |
| } | |
| /** | |
| * Run one `&&` / `||` chain. | |
| * | |
| * The grammar nests these to the right, while a shell evaluates them left to | |
| * right: `false && a || b` runs `b`. Flattening first is what makes the | |
| * skipped `&&` hand its status to the following `||` instead of taking the | |
| * whole remainder of the line with it. | |
| */ | |
| private async commandLine(commandLine: CommandLine, state: ShellState, io: ShellIo): Promise<number> { | |
| const links: { type: '&&' | '||'; chain: CommandChain }[] = [] | |
| for (let current = commandLine.then; current !== undefined; current = current.line.then) { | |
| links.push({ type: current.type, chain: current.line.chain }) | |
| } | |
| let status = await this.pipeline(commandLine.chain, state, io) | |
| state.lastStatus = status | |
| for (const link of links) { | |
| if (state.exitRequested !== undefined) return status | |
| if (link.type === '&&' ? status !== 0 : status === 0) continue | |
| status = await this.pipeline(link.chain, state, io) | |
| state.lastStatus = status | |
| } | |
| return status | |
| } | |
| /** Run one `|` / `|&` pipeline; its status is the last stage's. */ | |
| private async pipeline(chain: CommandChain, state: ShellState, io: ShellIo): Promise<number> { | |
| const stages: { command: CommandChain; mergesStderr: boolean }[] = [] | |
| for (let current: CommandChain | undefined = chain; current !== undefined;) { | |
| const link: CommandChain['then'] = current.then | |
| stages.push({ command: current, mergesStderr: link?.type === '|&' }) | |
| current = link?.chain | |
| } | |
| let input = io.stdin | |
| let status = 0 | |
| for (const [index, stage] of stages.entries()) { | |
| if (this.signal?.aborted === true) return ABORTED_STATUS | |
| const last = index === stages.length - 1 | |
| const piped = buffer() | |
| const stageIo: ShellIo = last | |
| ? { stdin: input, out: io.out, err: io.err } | |
| : { stdin: input, out: piped.write, err: stage.mergesStderr ? piped.write : io.err } | |
| status = await this.command(stage.command, state, stageIo) | |
| if (!last) input = piped.text() | |
| if (state.exitRequested !== undefined) return status | |
| } | |
| return status | |
| } | |
| /** Run one command node: a program call, a subshell, a group, or bare assignments. */ | |
| private async command(command: Command, state: ShellState, io: ShellIo): Promise<number> { | |
| switch (command.type) { | |
| case 'envs': | |
| for (const env of command.envs) assign(state, env.name, await this.assignedValue(env.args[0], state)) | |
| return 0 | |
| case 'subshell': { | |
| // A subshell sees a copy: its `cd` and its assignments die with it. | |
| const nested = { ...state, environment: { ...state.environment }, variables: { ...state.variables } } | |
| return await this.redirected(command.args, state, io, async inner => await this.line(command.subshell, nested, inner)) | |
| } | |
| case 'group': | |
| return await this.redirected(command.args, state, io, async inner => await this.line(command.group, state, inner)) | |
| case 'command': | |
| return await this.program(command, state, io) | |
| } | |
| } | |
| /** Expand a command's words and run the program they name. */ | |
| private async program(command: Extract<Command, { type: 'command' }>, state: ShellState, io: ShellIo): Promise<number> { | |
| const argv: string[] = [] | |
| const redirections: RedirectArgument[] = [] | |
| for (const argument of command.args) { | |
| if (argument.type === 'redirection') { | |
| redirections.push(argument) | |
| continue | |
| } | |
| argv.push(...await expandArgument(argument, this.context(state))) | |
| } | |
| const prefix: Record<string, string> = {} | |
| for (const env of command.envs) prefix[env.name] = await this.assignedValue(env.args[0], state) | |
| if (argv.length === 0) { | |
| for (const [name, value] of Object.entries(prefix)) assign(state, name, value) | |
| return 0 | |
| } | |
| // A prefixed command sees the assignments as environment for its run only, | |
| // which also means it cannot change the caller's directory. | |
| const scope = Object.keys(prefix).length === 0 | |
| ? state | |
| : { ...state, environment: { ...state.environment, ...prefix } } | |
| const name = argv[0] as string | |
| const program = this.programs.get(name) | |
| if (program === undefined) { | |
| io.err(`bash: ${name}: command not found\n`) | |
| return NOT_FOUND_STATUS | |
| } | |
| return await this.redirected(redirections, state, io, async (inner) => { | |
| try { | |
| return await program(argv, inner, scope, this.fs) | |
| } catch (error) { | |
| // A program's own defect must not take the whole worker down with it. | |
| inner.err(`bash: ${name}: ${error instanceof Error ? error.message : String(error)}\n`) | |
| return 1 | |
| } | |
| }) | |
| } | |
| /** | |
| * Apply redirections around one body, then restore nothing: every sink is a | |
| * value, so the caller's own `io` is untouched by construction. | |
| */ | |
| private async redirected( | |
| redirections: readonly RedirectArgument[], | |
| state: ShellState, | |
| io: ShellIo, | |
| body: (io: ShellIo) => Promise<number>, | |
| ): Promise<number> { | |
| let stdin = io.stdin | |
| let out = io.out | |
| let err = io.err | |
| // Every file write this redirection set started, awaited before the | |
| // command's status is reported: a `> file` must be complete on return. | |
| const writes: Promise<void>[] = [] | |
| for (const redirection of redirections) { | |
| const targets: string[] = [] | |
| for (const argument of redirection.args) targets.push(...await expandArgument(argument, this.context(state))) | |
| const target = targets[0] | |
| if (target === undefined || targets.length > 1) { | |
| io.err('bash: ambiguous redirect\n') | |
| return 1 | |
| } | |
| try { | |
| switch (redirection.subtype) { | |
| case '<': | |
| stdin = await this.fs.readText(resolveIn(state.cwd, target)) | |
| break | |
| case '<<<': | |
| stdin = `${target}\n` | |
| break | |
| case '>': | |
| case '>>': { | |
| const path = resolveIn(state.cwd, target) | |
| // Truncation happens at redirect time, so `> file` empties it even | |
| // when the command writes nothing. | |
| if (redirection.subtype === '>') await this.fs.writeText(path, '') | |
| // Appends are ordered by the queue below: a sink is synchronous to | |
| // its caller, so writes are chained rather than raced. | |
| let pending: Promise<void> = Promise.resolve() | |
| const sink = (text: string): void => { | |
| pending = pending.then(async () => { await this.fs.writeText(path, text, true) }) | |
| writes.push(pending) | |
| } | |
| if (redirection.fd === 2) err = sink | |
| else out = sink | |
| break | |
| } | |
| case '>&': { | |
| // Only descriptor duplication between stdout and stderr is | |
| // meaningful here: those are the only two the shell owns. | |
| if (redirection.fd === 2 && target === '1') err = out | |
| else if ((redirection.fd === null || redirection.fd === 1) && target === '2') out = err | |
| else { | |
| io.err(`bash: ${String(redirection.fd ?? 1)}>&${target}: unsupported descriptor redirection\n`) | |
| return 1 | |
| } | |
| break | |
| } | |
| case '<&': | |
| io.err(`bash: <&${target}: unsupported descriptor redirection\n`) | |
| return 1 | |
| } | |
| } catch (error) { | |
| io.err(`${describeFailure('bash', resolveIn(state.cwd, target), error)}\n`) | |
| return 1 | |
| } | |
| } | |
| const status = await body({ stdin, out, err }) | |
| await Promise.all(writes) | |
| return status | |
| } | |
| /** The expansion hook: `$( … )` runs on a nested interpreter of the same table. */ | |
| private context(state: ShellState): ExpansionContext { | |
| return { | |
| state, | |
| fs: this.fs, | |
| substitute: async (shell: ShellLine): Promise<string> => { | |
| if (this.depth >= MAX_SUBSTITUTION_DEPTH) { | |
| throw new Error(`command substitution nested deeper than ${String(MAX_SUBSTITUTION_DEPTH)} levels`) | |
| } | |
| const captured = buffer() | |
| const nested = { ...state, environment: { ...state.environment }, variables: { ...state.variables } } | |
| const inner = new Interpreter(this.programs, this.fs, this.signal, this.depth + 1) | |
| await inner.line(shell, nested, { stdin: '', out: captured.write, err: () => {} }) | |
| return captured.text().replace(/\n+$/, '') | |
| }, | |
| } | |
| } | |
| /** Expand the right-hand side of one `NAME=value` assignment. */ | |
| private async assignedValue(argument: ValueArgument | undefined, state: ShellState): Promise<string> { | |
| if (argument === undefined) return '' | |
| return (await expandArgument(argument, this.context(state))).join(' ') | |
| } | |
| } | |
| /** | |
| * Record one assignment. An exported name keeps its export (the environment | |
| * copy is what programs read); anything else stays a shell variable. | |
| */ | |
| function assign(state: ShellState, name: string, value: string): void { | |
| if (name in state.environment) state.environment[name] = value | |
| else state.variables[name] = value | |
| } | |