mirror of
https://github.com/harivansh-afk/asap.it.git
synced 2026-04-16 16:01:01 +00:00
first commit
This commit is contained in:
commit
1cdbffff09
200 changed files with 30007 additions and 0 deletions
220
app/utils/shell.ts
Normal file
220
app/utils/shell.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { withResolvers } from './promises';
|
||||
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) => {
|
||||
// console.log('terminal onData', { data, isInteractive });
|
||||
|
||||
if (isInteractive) {
|
||||
input.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
await jshReady.promise;
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
export type ExecutionResult = { output: string; exitCode: number } | undefined;
|
||||
|
||||
export class BoltShell {
|
||||
#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;
|
||||
|
||||
constructor() {
|
||||
this.#readyPromise = new Promise((resolve) => {
|
||||
this.#initialized = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
ready() {
|
||||
return this.#readyPromise;
|
||||
}
|
||||
|
||||
async init(webcontainer: WebContainer, terminal: ITerminal) {
|
||||
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?.();
|
||||
}
|
||||
|
||||
get terminal() {
|
||||
return this.#terminal;
|
||||
}
|
||||
|
||||
get process() {
|
||||
return this.#process;
|
||||
}
|
||||
|
||||
async executeCommand(sessionId: string, command: string): Promise<ExecutionResult> {
|
||||
if (!this.process || !this.terminal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const state = this.executionState.get();
|
||||
|
||||
/*
|
||||
* interrupt the current execution
|
||||
* this.#shellInputStream?.write('\x03');
|
||||
*/
|
||||
this.terminal.input('\x03');
|
||||
|
||||
if (state && state.executionPrms) {
|
||||
await state.executionPrms;
|
||||
}
|
||||
|
||||
//start a new execution
|
||||
this.terminal.input(command.trim() + '\n');
|
||||
|
||||
//wait for the execution to finish
|
||||
const executionPromise = this.getCurrentExecutionResult();
|
||||
this.executionState.set({ sessionId, active: true, executionPrms: executionPromise });
|
||||
|
||||
const resp = await executionPromise;
|
||||
this.executionState.set({ sessionId, active: false });
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
async getCurrentExecutionResult(): Promise<ExecutionResult> {
|
||||
const { output, exitCode } = await this.waitTillOscCode('exit');
|
||||
return { output, exitCode };
|
||||
}
|
||||
|
||||
async waitTillOscCode(waitCode: string) {
|
||||
let fullOutput = '';
|
||||
let exitCode: number = 0;
|
||||
|
||||
if (!this.#outputStream) {
|
||||
return { output: fullOutput, exitCode };
|
||||
}
|
||||
|
||||
const tappedStream = this.#outputStream;
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await tappedStream.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const text = value || '';
|
||||
fullOutput += text;
|
||||
|
||||
// Check if command completion signal with exit code
|
||||
const [, osc, , , code] = text.match(/\x1b\]654;([^\x07=]+)=?((-?\d+):(\d+))?\x07/) || [];
|
||||
|
||||
if (osc === 'exit') {
|
||||
exitCode = parseInt(code, 10);
|
||||
}
|
||||
|
||||
if (osc === waitCode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { output: fullOutput, exitCode };
|
||||
}
|
||||
}
|
||||
|
||||
export function newBoltShellProcess() {
|
||||
return new BoltShell();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue