UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

429 lines • 17.4 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 { 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