| 86 | | { id: string; seq: number; name: "exit"; value: number }; |
| 87 | |
| 88 | export class ShellWorker< |
| 89 | Env extends ShellWorkerEnv = ShellWorkerEnv, |
| 90 | > extends WorkerEntrypoint<Env> { |
| 91 | // Subclasses override to change the default cwd. |
| 92 | // ECMAScript module execution uses workspace.runtime; just-bash's Python and JavaScript |
| 93 | // commands require node:worker_threads and cannot run in workerd. |
| 94 | protected readonly shellOptions: ShellWorkerOptions = {}; |
| 95 | readonly #executions = new Map<string, AbortController>(); |
| 96 | |
| 97 | // Test seam: when set, exec uses this to spawn the command |
| 98 | // instead of `new Bash({...}).exec(...)`. Real production |
| 99 | // paths never touch it. |
| 100 | // |
| 101 | // The override receives the customCommands list that would |
| 102 | // have gone into the Bash constructor so tests can assert on |
| 103 | // it directly — the git command is registered here, and a |
| 104 | // subclass extraCommands hook would layer onto the same list. |
| 105 | protected bashFactoryOverride: |
| 106 | | (( |
| 107 | command: string, |
| 108 | options: { |
| 109 | cwd?: string; |
| 110 | env?: Record<string, string>; |
| 111 | stdin?: Uint8Array; |
| 112 | signal?: AbortSignal; |
| 113 | customCommands: CustomCommand[]; |
| 114 | }, |
| 115 | ) => Promise<{ stdout: string; stderr: string; exitCode: number }>) |
| 116 | | undefined; |
| 117 | |
| 118 | /** |
| 119 | * Subclass hook for registering additional custom commands |
| 120 | * alongside the built-in `git` command. Symmetric with |
| 121 | * `shellOptions`: the base class returns an empty list and |
| 122 | * subclasses override to layer their own commands on top. |
| 123 | * |
| 124 | * Called once per `exec` with the live host stub the shell |
| 125 | * already reached, so subclass commands share that stub's |
| 126 | * lifetime without needing to refetch. |
| 127 | */ |
| 128 | protected extraCommands(_ws: HostWorkspaceStub): CustomCommand[] { |
| 129 | return []; |
| 130 | } |
| 131 | |
| 132 | override async fetch(_request: Request): Promise<Response> { |
| 133 | return new Response( |
| 134 | "ShellWorker is invoked over Workers RPC — dispatch through the Workspace's WorkerShellBackend.", |
| 135 | { status: 426, headers: { "content-type": "text/plain; charset=utf-8" } }, |
| 136 | ); |
| 137 | } |
| 138 | |
| 139 | async exec(input: ExecInput): Promise<{ id: string; events: ReadableStream<Uint8Array> }> { |
| 140 | const id = input.id ?? crypto.randomUUID(); |
| 141 | const cwd = input.cwd ?? this.shellOptions.cwd ?? DEFAULT_CWD; |
| 142 | if (this.#executions.has(id)) |
| 143 | throw createShellError("EEXEC_BUSY", `execution ${id} is running`); |
| 144 | const controller = new AbortController(); |
| 145 | this.#executions.set(id, controller); |
nothing calls this directly
no outgoing calls
no test coverage detected