@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
145 lines • 4.74 kB
JavaScript
import { makeAbortError } from "../utils/abort.js";
import { concat } from "../utils/bytes.js";
import { OutputPipe } from "../io/output.js";
import { CAPACITY } from "../io/pipe.js";
import { JobConsole } from "../shell/console/job_console.js";
import { Channel } from "../shell/console/types.js";
import { PipeClosed } from "../io/errors.js";
import { ExecuteResult } from "./workspace/types.js";
class ShellOutput extends JobConsole {
pipe;
constructor(bufferBytes) {
super();
this.pipe = new OutputPipe(bufferBytes);
}
async emit(channel, data) {
if (channel === Channel.CONTROL)
return;
await this.pipe.write(channel, data);
}
}
class ShellEvents {
owner;
output;
source;
ended = false;
pulling = false;
constructor(owner, output) {
this.owner = owner;
this.output = output;
this.source = output.pipe.events();
}
[Symbol.asyncIterator]() {
return this;
}
async next() {
if (this.ended)
return { done: true, value: undefined };
if (this.pulling)
throw new Error('shell events already have a pending reader');
this.pulling = true;
try {
const next = await this.source.next();
if (this.output.pipe.closedReader || next.done) {
this.ended = true;
return { done: true, value: undefined };
}
return next;
}
catch (error) {
this.owner.cancel();
throw error;
}
finally {
this.pulling = false;
}
}
async return() {
await this.owner.close();
return { done: true, value: undefined };
}
async release() {
this.ended = true;
this.output.pipe.closeReader();
await this.source.return(undefined);
}
}
/** A shell invocation and bounded single-reader events. Drain events before wait(). */
export class ShellExecution {
events;
controller = new AbortController();
output;
result;
closing;
settled = false;
constructor(run, scope, signal, bufferBytes = CAPACITY) {
this.output = new ShellOutput(bufferBytes);
this.events = new ShellEvents(this, this.output);
const combined = signal === undefined
? this.controller.signal
: AbortSignal.any([signal, this.controller.signal]);
this.result = this.run(run, scope, combined);
// The outcome is delivered by wait()/events even when a consumer closes before its first pull.
void this.result.catch(() => undefined);
}
async run(run, scope, signal) {
try {
if (signal.aborted)
throw makeAbortError(signal);
const result = await run(this.output, signal, scope);
this.output.pipe.end();
return result;
}
catch (error) {
const failure = signal.aborted && error instanceof PipeClosed ? makeAbortError(signal) : error;
this.output.pipe.end(failure);
throw failure;
}
finally {
this.settled = true;
}
}
/** @internal Release workspace registration when invocation settles. */
onSettled(callback) {
void this.result.then(callback, callback);
}
cancel() {
if (!this.settled) {
this.controller.abort(new DOMException('execute aborted', 'AbortError'));
this.output.pipe.end(this.controller.signal.reason);
}
}
/** Join the invocation; final stdout/stderr are empty because events delivered them. */
wait() {
return this.result;
}
/** Collect output and await an optional observer before the next pull. */
async collect(onEvent) {
const stdout = [];
const stderr = [];
try {
for await (const event of this.events) {
;
(event.stream === Channel.STDOUT ? stdout : stderr).push(event.data);
await onEvent?.(event);
}
const result = await this.wait();
return new ExecuteResult(concat(stdout), concat(stderr), result.exitCode, result.refusal);
}
finally {
await this.close();
}
}
/** Discard unread output, cancel and join under the shell's normal cancellation policy. */
close() {
this.closing ??= this.closeOwned();
return this.closing;
}
async closeOwned() {
this.cancel();
await this.events.release();
await Promise.allSettled([this.result]);
await this.output.close();
}
}
//# sourceMappingURL=shell_execution.js.map