@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
176 lines • 6.8 kB
JavaScript
// ========= 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