UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

176 lines • 6.8 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 { encodeText } from "../../../../shell/bytes.js"; import { isEisdir, isFsError } from "../../../../errors/fs.js"; import { chunks } from "../../../../io/cooperative.js"; import { OutputStream, closeQuietly } from "../../../../io/stream.js"; import { IOResult } from "../../../../io/types.js"; import { YieldBudget } from "../../../../io/yield_budget.js"; import { openStream } from "../../../../rs/stream.js"; import { FileType } from "../../../../types.js"; import { concat } from "../../../../utils/bytes.js"; import { command } from "../../../config.js"; import { commandFiles } from "../../generic_bind/files.js"; import { specOf } from "../../../spec/builtins.js"; import { FlagView } from "../../../spec/flag_view.js"; /** * The diagnostics so far. Each read starts its own pass over them, so * concurrent readers each read all of them. */ class Diagnostics { parts; count; constructor(parts, count) { this.parts = parts; this.count = count; } async *[Symbol.asyncIterator]() { await Promise.resolve(); yield* this.parts.slice(0, this.count); } } /** Collect diagnostics before yielding output and drain pending work. */ export async function* drained(stream, output, io, budget, errors) { for (;;) { io.exitCode = stream.status(); const notice = stream.takeStderr(); if (notice.byteLength > 0) { errors.push(notice); io.stderr = new Diagnostics(errors, errors.length); } if (output.byteLength > 0) yield output; if (!stream.busy()) return; await budget.run(); output = stream.drain(); } } /** * One operand bc runs ahead of standard input, or what it says when it * cannot read it. */ export async function readFile(path, ctx) { const unreadable = `${path.rawPath} 1: read() in flex scanner failed\n`; try { if (ctx.vfs === undefined) return [new Uint8Array(), `File ${path.rawPath} is unavailable.\n`]; // bc opens a directory and fails reading it; asked first, since a store // may answer a directory's read as missing, or with bytes. if ((await ctx.vfs.stat(path)).type === FileType.DIRECTORY) return [new Uint8Array(), unreadable]; return [await ctx.vfs.read(path), null]; } catch (err) { if (isEisdir(err)) return [new Uint8Array(), unreadable]; if (isFsError(err)) return [new Uint8Array(), `File ${path.rawPath} is unavailable.\n`]; throw err; } } /** * bc's file operands with their globs expanded, as the shell would have; a * word that matches nothing stays as typed, and bc cannot open it. */ export async function expanded(paths, ctx) { if (ctx.vfs === undefined || paths.every((path) => path.resolved)) return paths; return commandFiles(ctx, paths).resolveGlob(paths); } export function bc(_accessor, paths, texts, ctx) { const fl = new FlagView(ctx.flags, specOf('bc')); const config = { math_mode: fl.asBool('args_l'), line_length: ctx.env?.BC_LINE_LENGTH ?? null, files: [], }; const source = ctx.stdin ?? new Uint8Array(); const io = new IOResult(); const errors = []; let inputStarted = false; const input = source instanceof Uint8Array ? source : { [Symbol.asyncIterator]() { const iterator = source[Symbol.asyncIterator](); inputStarted = true; return iterator; }, }; async function* output() { // GNU bc runs the files in order, then standard input, as one program. // It opens a file once the ones before it have run, so quit or halt // leaves the rest unread, and a file it cannot read ends the run there, // exit 1. const operands = await expanded(paths, ctx); // A name is bytes, which travel one character each. config.files = operands.map((path) => String.fromCharCode(...encodeText(path.rawPath))); const stream = await openStream('bc', config); const budget = new YieldBudget(ctx.signal); async function* fed(piece) { for await (const chunk of chunks(piece, ctx.signal)) { await budget.run(); yield* drained(stream, stream.feed(chunk), io, budget, errors); if (stream.done()) return; } } let refusal = null; try { for (const [index, path] of operands.entries()) { if (stream.done()) break; const [data, refused] = await readFile(path, ctx); refusal = refused; if (refusal !== null) break; yield* fed(data); yield* drained(stream, stream.endInput(index), io, budget, errors); } if (refusal !== null) { // bc stops at the file it cannot open: a statement or comment the // files before it left open never runs. errors.push(encodeText(refusal)); io.stderr = new Diagnostics(errors, errors.length); io.exitCode = 1; return; } if (!stream.done()) yield* fed(input); yield* drained(stream, stream.finish(), io, budget, errors); } finally { // Snapshots serve reads while output drains; once it stops, the // command record copies stderr only as bytes, so join it once. if (errors.length > 0) io.stderr = concat(errors); stream.close(); } } return Promise.resolve([ new OutputStream(output(), async () => { if (!inputStarted) await closeQuietly(source); }), io, ]); } export const GENERAL_BC = command({ name: 'bc', vfs: null, spec: specOf('bc'), fn: bc, }); //# sourceMappingURL=command.js.map