bolt.diy/app/utils/shell.ts

221 lines
5.6 KiB
TypeScript
Raw Normal View History

2024-11-08 16:17:31 +00:00
import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
import type { ITerminal } from '~/types/terminal';
import { withResolvers } from './promises';
2024-11-08 16:17:31 +00:00
import { atom } from 'nanostores';
export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
const args: string[] = [];
// we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal
const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {
terminal: {
cols: terminal.cols ?? 80,
rows: terminal.rows ?? 15,
},
});
const input = process.input.getWriter();
const output = process.output;
const jshReady = withResolvers<void>();
let isInteractive = false;
output.pipeTo(
new WritableStream({
write(data) {
if (!isInteractive) {
const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || [];
if (osc === 'interactive') {
// wait until we see the interactive OSC
isInteractive = true;
jshReady.resolve();
}
}
terminal.write(data);
},
}),
);
terminal.onData((data) => {
2024-11-08 16:17:31 +00:00
// console.log('terminal onData', { data, isInteractive });
if (isInteractive) {
input.write(data);
}
});
await jshReady.promise;
return process;
}
2024-11-08 16:17:31 +00:00
2024-11-21 21:27:29 +00:00
export type ExecutionResult = { output: string; exitCode: number } | undefined;
2024-11-08 16:17:31 +00:00
export class BoltShell {
2024-11-21 21:05:35 +00:00
#initialized: (() => void) | undefined;
#readyPromise: Promise<void>;
#webcontainer: WebContainer | undefined;
#terminal: ITerminal | undefined;
#process: WebContainerProcess | undefined;
executionState = atom<{ sessionId: string; active: boolean; executionPrms?: Promise<any> } | undefined>();
#outputStream: ReadableStreamDefaultReader<string> | undefined;
#shellInputStream: WritableStreamDefaultWriter<string> | undefined;
2024-11-21 21:27:29 +00:00
2024-11-08 16:17:31 +00:00
constructor() {
this.#readyPromise = new Promise((resolve) => {
2024-11-21 21:05:35 +00:00
this.#initialized = resolve;
});
2024-11-08 16:17:31 +00:00
}
2024-11-21 21:27:29 +00:00
2024-11-08 16:17:31 +00:00
ready() {
return this.#readyPromise;
}
2024-11-21 21:27:29 +00:00
2024-11-08 16:17:31 +00:00
async init(webcontainer: WebContainer, terminal: ITerminal) {
2024-11-21 21:05:35 +00:00
this.#webcontainer = webcontainer;
this.#terminal = terminal;
const { process, output } = await this.newBoltShellProcess(webcontainer, terminal);
this.#process = process;
this.#outputStream = output.getReader();
await this.waitTillOscCode('interactive');
this.#initialized?.();
2024-11-08 16:17:31 +00:00
}
2024-11-21 21:27:29 +00:00
2024-11-08 16:17:31 +00:00
get terminal() {
2024-11-21 21:05:35 +00:00
return this.#terminal;
2024-11-08 16:17:31 +00:00
}
2024-11-21 21:27:29 +00:00
2024-11-08 16:17:31 +00:00
get process() {
2024-11-21 21:05:35 +00:00
return this.#process;
2024-11-08 16:17:31 +00:00
}
2024-11-21 21:27:29 +00:00
async executeCommand(sessionId: string, command: string): Promise<ExecutionResult> {
2024-11-08 16:17:31 +00:00
if (!this.process || !this.terminal) {
2024-11-21 21:27:29 +00:00
return undefined;
2024-11-08 16:17:31 +00:00
}
2024-11-21 21:05:35 +00:00
const state = this.executionState.get();
/*
* interrupt the current execution
* this.#shellInputStream?.write('\x03');
*/
this.terminal.input('\x03');
2024-11-21 21:05:35 +00:00
if (state && state.executionPrms) {
2024-11-21 21:05:35 +00:00
await state.executionPrms;
2024-11-08 16:17:31 +00:00
}
2024-11-21 21:05:35 +00:00
//start a new execution
2024-11-08 16:17:31 +00:00
this.terminal.input(command.trim() + '\n');
//wait for the execution to finish
2024-11-21 21:27:29 +00:00
const executionPromise = this.getCurrentExecutionResult();
this.executionState.set({ sessionId, active: true, executionPrms: executionPromise });
2024-11-21 21:27:29 +00:00
const resp = await executionPromise;
2024-11-21 21:05:35 +00:00
this.executionState.set({ sessionId, active: false });
2024-11-08 16:17:31 +00:00
2024-11-21 21:05:35 +00:00
return resp;
2024-11-08 16:17:31 +00:00
}
2024-11-21 21:27:29 +00:00
2024-11-08 16:17:31 +00:00
async newBoltShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
const args: string[] = [];
// we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal
const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {
terminal: {
cols: terminal.cols ?? 80,
rows: terminal.rows ?? 15,
},
});
const input = process.input.getWriter();
this.#shellInputStream = input;
2024-11-21 21:05:35 +00:00
2024-11-08 16:17:31 +00:00
const [internalOutput, terminalOutput] = process.output.tee();
const jshReady = withResolvers<void>();
let isInteractive = false;
terminalOutput.pipeTo(
new WritableStream({
write(data) {
if (!isInteractive) {
const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || [];
if (osc === 'interactive') {
// wait until we see the interactive OSC
isInteractive = true;
jshReady.resolve();
}
}
terminal.write(data);
},
}),
);
terminal.onData((data) => {
// console.log('terminal onData', { data, isInteractive });
if (isInteractive) {
input.write(data);
}
});
await jshReady.promise;
return { process, output: internalOutput };
}
2024-11-21 21:27:29 +00:00
async getCurrentExecutionResult(): Promise<ExecutionResult> {
2024-11-21 21:05:35 +00:00
const { output, exitCode } = await this.waitTillOscCode('exit');
return { output, exitCode };
}
2024-11-21 21:27:29 +00:00
async waitTillOscCode(waitCode: string) {
2024-11-08 16:17:31 +00:00
let fullOutput = '';
let exitCode: number = 0;
2024-11-21 21:05:35 +00:00
if (!this.#outputStream) {
return { output: fullOutput, exitCode };
}
const tappedStream = this.#outputStream;
2024-11-08 16:17:31 +00:00
while (true) {
const { value, done } = await tappedStream.read();
2024-11-21 21:05:35 +00:00
if (done) {
break;
}
2024-11-08 16:17:31 +00:00
const text = value || '';
fullOutput += text;
// Check if command completion signal with exit code
2024-11-21 21:27:29 +00:00
const [, osc, , , code] = text.match(/\x1b\]654;([^\x07=]+)=?((-?\d+):(\d+))?\x07/) || [];
2024-11-21 21:05:35 +00:00
if (osc === 'exit') {
exitCode = parseInt(code, 10);
}
2024-11-21 21:05:35 +00:00
if (osc === waitCode) {
2024-11-08 16:17:31 +00:00
break;
}
}
2024-11-21 21:05:35 +00:00
2024-11-08 16:17:31 +00:00
return { output: fullOutput, exitCode };
}
}
2024-11-21 21:27:29 +00:00
2024-11-08 16:17:31 +00:00
export function newBoltShellProcess() {
return new BoltShell();
}