UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

199 lines • 7.87 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { IOResult } from "../../io/types.js"; import { Terminal } from "../../shell/console/index.js"; import { concat } from "../../utils/bytes.js"; import { historyEntries } from "./entries.js"; /** What a line's history has recorded so far, whichever root reads the line. */ class Recording { observer; command; entries; sessionId; session; id; terminal; current = -1; opened = 0; line; ops = 0; whole = false; through = false; done = false; constructor(observer, command, entries, sessionId, session, id, terminal) { this.observer = observer; this.command = command; this.entries = entries; this.sessionId = sessionId; this.session = session; this.id = id; this.terminal = terminal; } async begin(text) { this.line = this.opened === 0 || this.id === undefined ? this.id : `${this.id}.${String(this.opened)}`; this.opened += 1; await this.observer.logCommand(text, this.sessionId, this.session.cwd, this.line); } async close(io, records) { if (this.opened === 0) return; const [stdout, stderr] = this.terminal.entry(); await this.observer.logResult(new IOResult({ stdout: concat([stdout, await io.materializeStdout()]), stderr: concat([stderr, await io.materializeStderr()]), exitCode: io.exitCode, }), records.slice(this.ops), this.sessionId, this.session.cwd, this.line); this.ops = records.length; } } /** * A typed line's history, recorded as bash's line reader adds it: an entry * per command the line reads, each with what it wrote to the terminal and * the status it left, where a line of one command is recorded whole. The * line's root program reaches each command as it runs it (`reach`), and * the line's end ends the last (`end`). A line answered before its tree * runs, or run whole on a runtime (`typed`), is recorded once, as typed. * Input the root reads again goes on with the root it is read as, which * the history `readOn` gives it records. Mirrors Python's LineHistory. */ export class LineHistory { recording; root; shift; constructor(recording, // The root that reads the line; null until the first to run takes it. root, // Where the root's input starts in the line as typed. shift) { this.recording = recording; this.root = root; this.shift = shift; } /** * The history of a line whose input bash's reader reads as more than * one entry, written to a terminal; null for any other line, which the * line records whole as it starts. * * @param observer the workspace's observer. * @param command the line as typed. * @param root the parsed line. * @param sessionId the session the line is recorded under. * @param session the session the line runs on, whose cwd its entries name. * @param id the line's execution id, its first entry's. * @param sink where the line writes. */ static open(observer, command, root, sessionId, session, id, sink) { if (!(sink instanceof Terminal)) return null; const entries = historyEntries(root); if (entries.length < 2) return null; return new LineHistory(new Recording(observer, command, entries, sessionId, session, id, sink), root, 0); } /** * Whether `node`'s program reads the line: this history's root, or, for * the history of input read again (`readOn`), the first root to run. * * @param node a program about to run its statements. */ reads(node) { if (this.root === null && (node.parent ?? null) === null) this.root = node; return node === this.root; } /** * The history of the root's input read again from a statement on, as a * root of its own: bound to the call that reads it, whose first root * takes the line over. * * @param at where, in the root's input, the input read again starts: * the statement's index less the blank rows put ahead of it. */ readOn(at) { return new LineHistory(this.recording, null, this.shift + at); } /** * Reach the command the root program runs next: end the entry before it * and record the entries up to it, its own last. * * @param child the root's statement about to run. * @param session the shell running it, whose `$?` ends the entry before. * @param records the line's op records so far, of which the entry * before's are those since the one before it ended. */ async reach(child, session, records) { const line = this.recording; if (line.whole || line.done) return; const start = child.startIndex ?? 0; const at = this.shift + (this.root?.offsets?.[start] ?? start); let reached = line.current; for (let j = line.current + 1; j < line.entries.length; j++) { const entry = line.entries[j]; if (entry === undefined || (entry.at !== null && entry.at > at)) break; if (entry.at !== null) reached = j; } if (reached <= line.current) return; const status = new IOResult({ exitCode: session.lastExitCode }); await line.close(status, records); for (let j = line.current + 1; j < reached; j++) { await line.begin(line.entries[j]?.text ?? ''); await line.close(status, records); } line.current = reached; await line.begin(line.entries[reached]?.text ?? ''); } /** The root program read the input to its end, so the entries after its last command are read too. */ read() { this.recording.through = true; } /** Record the line once, as typed, before it runs whole on a runtime. */ async typed() { const line = this.recording; if (line.current >= 0 || line.whole) return; line.whole = true; await line.begin(line.command); } /** * End the line: its last entry, with what the line wrote since and what * it answered with besides, or, when no command was reached, the line as * typed, unless its root read it through. A line ends once. * * @param io the line's result. * @param records the line's op records, of which the last entry's are * those since the entry before ended. */ async end(io, records) { const line = this.recording; if (line.done) return; if (line.current < 0 && !line.through) await this.typed(); await line.close(io, records); line.done = true; if (!line.through || line.whole) return; const status = new IOResult({ exitCode: io.exitCode }); for (let j = line.current + 1; j < line.entries.length; j++) { await line.begin(line.entries[j]?.text ?? ''); await line.close(status, records); } } } //# sourceMappingURL=line.js.map