@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
429 lines • 17.4 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 { OutputStream, closeQuietly } from "../../../io/stream.js";
import { IOResult, materialize } from "../../../io/types.js";
import { chunks } from "../../../io/cooperative.js";
import { ByteWriter } from "../../../io/writer.js";
import { PipeClosed } from "../../../io/errors.js";
import { LineFacts } from "../../../types.js";
import { eisdir, enotdir, fsError, isEnoent, isFsError } from "../../../errors/fs.js";
import { fsErrorLine } from "../../../errors/render.js";
import { posixPhrase } from "../../../errors/posix.js";
import {} from "../../../errors/types.js";
import { absentDestError, entryKind } from "../utils/paths.js";
import { rstripSlash } from "../../../utils/slash.js";
import { readStdinAsync } from "../utils/stream.js";
import { specOf } from "../../spec/builtins.js";
import { FlagView } from "../../spec/flag_view.js";
import {} from "../../spec/types.js";
import { encodeText } from "../../../shell/bytes.js";
import { linksOf, mountRoot } from "../utils/links.js";
import { mountKey } from "../../../utils/key_prefix.js";
import { SIGPIPE_STATUS } from "../../../shell/constants.js";
/**
* How tee answers a write into a pipe whose reader has gone: SIGPIPE without
* -p and --output-error, nothing under a -nopipe mode, a write error's report
* under warn and exit. Mirrors Python's PipeErrors.
*/
export var PipeErrors;
(function (PipeErrors) {
PipeErrors["SIGNAL"] = "signal";
PipeErrors["IGNORE"] = "ignore";
PipeErrors["REPORT"] = "report";
})(PipeErrors || (PipeErrors = {}));
export function parseFlags(bag) {
// --output-error values are validated declaratively: the spec's
// choices= makes the parser report any other value and the executor
// refuse with GNU's invalid-argument lines before tee runs. -p and a bare
// --output-error are warn-nopipe; the last one typed wins, as GNU's one
// option does.
const fl = new FlagView(bag, specOf('tee'));
let mode = null;
for (const [, value] of fl.occurrences('p', 'output_error')) {
mode = typeof value === 'string' ? value : 'warn-nopipe';
}
return {
append: fl.asBool('append'),
stopOnError: mode === 'exit' || mode === 'exit-nopipe',
pipeErrors: mode === null
? PipeErrors.SIGNAL
: mode.endsWith('-nopipe')
? PipeErrors.IGNORE
: PipeErrors.REPORT,
};
}
export async function teeGeneric(paths, texts, ctx, stream, write, append, stat, outputKey) {
const parsed = parseFlags(ctx.flags);
const root = mountRoot(ctx);
if (ctx.stdin != null && !(ctx.stdin instanceof Uint8Array)) {
const opened = [];
const [, io] = await writeOutput(paths, new Uint8Array(), parsed, stream, write, append, stat, opened, root);
io.closedStdout = closedStdout(parsed);
if (io.exitCode !== 0 && parsed.stopOnError) {
await closeQuietly(ctx.stdin);
return [null, io];
}
const source = ctx.stdin;
const parked = { atYield: false };
return [
new OutputStream(teeStream(source, opened, parsed, stream, write, append, io, outputKey ?? ((p) => linksOf(ctx)?.resolve(p.virtual) ?? p.virtual), parked, root)[Symbol.asyncIterator](),
// Parked at a yield, tee's own return copies what its files still
// take and closes its input; otherwise nothing would.
() => (parked.atYield ? Promise.resolve() : closeQuietly(source))),
io,
];
}
const stdinData = await readStdinAsync(ctx.stdin);
const raw = stdinData ?? encodeText(texts.join(' '));
const [stdout, io] = paths.length === 0
? [raw, new IOResult()]
: await writeOutput(paths, raw, parsed, stream, write, append, stat, undefined, root);
io.closedStdout = closedStdout(parsed);
return [stdout, io];
}
/**
* How tee answers its stdout's reader going away, as GNU's does under its
* `PipeErrors` mode. Mirrors Python's _closed_stdout.
*/
function closedStdout(parsed) {
if (parsed.pipeErrors === PipeErrors.SIGNAL)
return null;
if (parsed.pipeErrors === PipeErrors.IGNORE)
return { stderr: new Uint8Array(), exitCode: null };
return {
stderr: encodeText(`tee: 'standard output': ${posixPhrase('EPIPE')}\n`),
exitCode: 1,
};
}
/**
* Write one operand. A native append sends only the new bytes, which costs
* one read on the next access and saves reading and re-uploading the whole
* object on this one.
*/
async function writeOne(path, raw, parsed, stream, write, append) {
if (!parsed.append) {
await write(path, raw);
return;
}
if (append !== undefined) {
await append(path, raw);
return;
}
let existing = new Uint8Array(0);
try {
existing = await materialize(stream(path));
}
catch (err) {
// GNU tee -a creates a missing file: append to empty. This used to test
// the message for /not found/i, which never matched — `enoent()` puts the
// *path* in the message — so `tee -a missing` threw on every backend, and
// s3/gridfs grew bespoke wrappers with an exists() pre-check to dodge it.
if (!isEnoent(err))
throw err;
}
const data = new Uint8Array(existing.byteLength + raw.byteLength);
data.set(existing, 0);
data.set(raw, existing.byteLength);
await write(path, data);
}
/**
* Copy `raw` to every operand, GNU-style.
*
* An operand that cannot be written is diagnosed and skipped rather than ending
* the run: GNU keeps going and still writes the rest, and only
* `--output-error=exit` stops at the first failure. stdin always reaches stdout
* either way. The operand is named as typed and the strerror comes from the
* shared table, so an unwritable destination reads like GNU rather than
* exposing the backend's own exception text.
*
* Deliberate divergence: GNU opens every operand up front, so under `exit` an
* *open* failure aborts before any data is written. A mount has no open/write
* split — `write` is one call — so the operands before the failure are already
* written. The two agree whenever the failure is at write time, which is what a
* remote backend reports.
*/
// GNU's diagnostic for one unwritable operand, a transport error naming it
// below `root`, the mount the command runs on. Mirrors Python's error_line.
function errorLine(path, err, root = '') {
if (isFsError(err))
return fsErrorLine('tee', path, err);
const shown = `/${mountKey(path.virtual, root)}`;
return `tee: ${shown}: ${err instanceof Error ? err.message : String(err)}\n`;
}
// The error GNU's open of an output would meet, or null. GNU opens the
// outputs in order, so each earlier one is a regular file by now: an output
// under one of them is `Not a directory`. Mirrors Python's open_refusal.
export async function openRefusal(stat, path, opened) {
if (opened.some((o) => path.virtual.startsWith(`${rstripSlash(o.virtual)}/`))) {
return enotdir(path);
}
const { exists, isDir } = await entryKind(stat, path);
if (isDir)
return eisdir(path);
if (exists)
return null;
const condition = await absentDestError(stat, path);
if (condition === null)
return null;
return fsError(path, condition);
}
export async function writeOutput(paths, raw, parsed, stream, write, append, stat, openedPaths, root = '') {
const errors = [];
// GNU opens every output before it reads a byte: under exit the first open
// failure ends the run with nothing written, the outputs before it made
// empty. A mount write is one call, so the open is probed first. A probe
// the backend will not answer (a stat its credentials refuse) is no
// verdict, so that output is opened for real, by writing it nothing, once
// the outputs before it are opened. Opening an earlier output can fail
// first, and then it is the one reported.
if (parsed.stopOnError && stat !== undefined) {
const opened = new Set();
for (const [index, path] of paths.entries()) {
let probed = true;
let refusal = null;
try {
refusal = await openRefusal(stat, path, paths.slice(0, index));
}
catch (err) {
console.warn(`tee: probing ${path.virtual} failed: ${String(err)}`);
probed = false;
}
if (probed && refusal === null)
continue;
let failed = path;
for (const prior of paths.slice(0, index)) {
if (opened.has(prior.virtual))
continue;
try {
if (!(parsed.append && (await entryKind(stat, prior)).exists)) {
await write(prior, new Uint8Array(0));
}
}
catch (err) {
failed = prior;
refusal = err;
break;
}
opened.add(prior.virtual);
}
if (refusal === null) {
try {
await writeOne(path, new Uint8Array(0), parsed, stream, write, append);
opened.add(path.virtual);
continue;
}
catch (err) {
refusal = err;
}
}
const stderr = encodeText(errorLine(failed, refusal, root));
return [null, new IOResult({ exitCode: 1, stderr })];
}
}
for (const [index, path] of paths.entries()) {
// A store keeps a key over a directory or under a file, where an open
// would fail, so the open is probed for every output, not only the ones
// --output-error=exit probed above. A missing parent is the write's to
// refuse, which a keyed store makes as a redirect does.
let refusal = null;
// Under exit every output was proved or opened above.
let proved = stat !== undefined && parsed.stopOnError;
if (stat !== undefined && !parsed.stopOnError) {
try {
refusal = await openRefusal(stat, path, paths.slice(0, index));
proved = refusal === null;
}
catch (err) {
console.warn(`tee: probing ${path.virtual} failed: ${String(err)}`);
}
}
if (refusal !== null && refusal.code !== 'ENOENT') {
errors.push(errorLine(path, refusal, root));
continue;
}
if (openedPaths !== undefined && proved) {
// A streamed output's own write creates it; writing it nothing first
// would cost the store a write (a PUT, a revision).
openedPaths.push(path);
continue;
}
try {
await writeOne(path, raw, parsed, stream, write, append);
openedPaths?.push(path);
}
catch (err) {
if (err instanceof PipeClosed) {
// A pipe operand whose reader has gone: SIGPIPE ends tee with what it
// reported, unless -p or --output-error say otherwise.
if (parsed.pipeErrors === PipeErrors.SIGNAL) {
const stderr = errors.length > 0 ? encodeText(errors.join('')) : null;
return [null, new IOResult({ exitCode: SIGPIPE_STATUS, stderr })];
}
if (parsed.pipeErrors === PipeErrors.IGNORE)
continue;
errors.push(errorLine(path, fsError(path, 'EPIPE'), root));
}
else
errors.push(errorLine(path, err, root));
if (parsed.stopOnError)
break;
}
}
if (errors.length > 0) {
return [raw, new IOResult({ exitCode: 1, stderr: encodeText(errors.join('')) })];
}
return [raw, new IOResult()];
}
async function consumeOutput(path, parsed, stream, write, append, source) {
if (!parsed.append)
await write(path, source);
else if (append !== undefined)
await append(path, source);
else {
let existing = new Uint8Array();
try {
existing = await materialize(stream(path));
}
catch (error) {
if (!isEnoent(error))
throw error;
}
async function* joined() {
yield existing;
yield* chunks(source);
}
await write(path, joined());
}
}
async function* teeStream(source, paths, parsed, stream, write, append, io, outputKey, parked, root) {
const writers = new Map();
const errors = [await io.stderrStr()];
const failed = (path, error) => {
if (error instanceof PipeClosed) {
if (parsed.pipeErrors === PipeErrors.SIGNAL) {
io.exitCode = SIGPIPE_STATUS;
return true;
}
if (parsed.pipeErrors === PipeErrors.IGNORE)
return false;
error = fsError(path, 'EPIPE');
}
errors.push(errorLine(path, error, root));
io.exitCode = 1;
return parsed.stopOnError;
};
for (const path of paths) {
const key = outputKey(path);
const prior = writers.get(key);
if (prior !== undefined) {
if (parsed.append)
prior.copies++;
}
else
writers.set(key, {
path,
copies: 1,
writer: new ByteWriter((input) => consumeOutput(path, parsed, stream, write, append, input)),
});
}
const feed = async (chunk) => {
for (const [key, { path, writer, copies }] of writers) {
try {
for (let n = 0; n < copies; n++)
await writer.write(chunk);
}
catch (error) {
await writer.abort();
writers.delete(key);
if (failed(path, error))
return false;
}
}
return true;
};
const input = chunks(source)[Symbol.asyncIterator]();
let interrupted = false;
let closed = true;
try {
// GNU's tee opens every output before it reads: a refused one is
// reported (or ends it under exit) while the input is quiet.
for (const [key, { path, writer }] of writers) {
try {
await writer.opened();
}
catch (error) {
await writer.abort();
writers.delete(key);
if (failed(path, error)) {
closed = false;
await closeQuietly(source);
return;
}
}
}
for (;;) {
const next = await input.next();
if (next.done === true || !(await feed(next.value))) {
closed = false;
return;
}
parked.atYield = true;
yield next.value;
parked.atYield = false;
}
}
catch (error) {
closed = false;
interrupted = true;
throw error;
}
finally {
// stdout's reader left: GNU's tee goes on copying its input to the
// files unless that ends it (SIGPIPE, or exit).
if (closed &&
parsed.pipeErrors !== PipeErrors.SIGNAL &&
!(parsed.pipeErrors === PipeErrors.REPORT && parsed.stopOnError)) {
try {
while (writers.size > 0) {
const next = await input.next();
if (next.done === true || !(await feed(next.value)))
break;
}
}
catch (error) {
console.debug('tee: input failed after its reader left:', error);
interrupted = true;
}
}
await input.return?.();
for (const { path, writer } of writers.values()) {
try {
if (interrupted)
await writer.abort();
else
await writer.finish();
}
catch (error) {
failed(path, error);
}
}
const stderr = errors.join('');
io.stderr = stderr.length > 0 ? encodeText(stderr) : null;
}
}
/** A tee line writes the files it names. Mirrors Python's `facts`. */
export function facts(fl, operands) {
return new LineFacts({ writes: operands.length > 0 });
}
//# sourceMappingURL=tee.js.map