@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
137 lines • 5.69 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, ensureStream } from "../../../io/stream.js";
import { closeStream, streamOutputChunks } from "../../../rs/stream.js";
import { materialize } from "../../../io/types.js";
import { isEisdir } from "../../../errors/fs.js";
import { FileStat, FileType } from "../../../types.js";
import { encodeText } from "../../../shell/bytes.js";
import { STDIN_HEADER_NAME } from "./constants.js";
import { mergeSignals } from "../../../utils/abort.js";
export async function readStdinAsync(stdin) {
return stdin === null ? null : materialize(stdin);
}
export function resolveSource(stdin) {
// GNU semantics: no stdin behaves like empty input (/dev/null)
return ensureStream(stdin ?? new Uint8Array(0));
}
/**
* Whether an operand reads stdin. `dash` says a literal `-` names stdin, as
* it does for most GNU tools; util-linux `rev` and binutils `strings` open it
* as a file, so only /dev/stdin is stdin to them.
*/
export function isStdin(path, dash = true) {
return (dash && path.rawPath === '-') || path.virtual === '/dev/stdin';
}
/**
* Whether a failed read means the operand never opened: nothing arrived from
* it, and it is neither stdin, which is always open, nor a directory, which
* opens and then fails to read. GNU names such an operand as it does a
* missing one, not as a read error. Mirrors Python's never_opened.
*/
export function neverOpened(reader, path, error) {
return reader.loaded === 0 && !isStdin(path) && !isEisdir(error);
}
/**
* The name a command's output gives an operand. Only a literal `-` is stdin by
* name: /dev/stdin reads the same bytes, but GNU grep, head and tail name it as
* the path it is.
*/
export function operandLabel(path, stdinName) {
return path.rawPath === '-' ? stdinName : path.rawPath;
}
/**
* The `==> name <==` banner head and tail print above an operand: null names
* the stdin no operand named, which -v heads with the name `-` gets, and
* `after` says a block printed before this one, which a blank line then
* separates it from. Mirrors Python's operand_header.
*/
export function operandHeader(path, after = false) {
const label = path === null ? STDIN_HEADER_NAME : operandLabel(path, STDIN_HEADER_NAME);
return encodeText(`${after ? '\n' : ''}==> ${label} <==\n`);
}
/**
* Whether head or tail banners its operands: under -v or for more than one
* operand (a failed one included), never under -q. Mirrors Python's
* shows_headers.
*/
export function showsHeaders(verbose, quiet, count) {
return (verbose || count > 1) && !quiet;
}
/**
* Read each operand from its backend, or from stdin for a stdin one.
*
* Every stdin operand shares one cursor, so a later `-` never replays bytes
* an earlier one read, and the cursor never closes the input a later one may
* still read. `sole` says stdin has exactly one reader, which takes the input
* itself, so a scan that stops early closes it. `dash` is as in `isStdin`.
*/
export function stdinStream(read, stdin, sole = false, dash = true) {
const input = resolveSource(stdin);
const source = input[Symbol.asyncIterator]();
async function* inputStream() {
// All '-' operands share one cursor; a new operand must not replay bytes.
for (;;) {
const next = await source.next();
if (next.done === true)
return;
yield next.value;
}
}
// Bind the backend stream while its mount cache context is active.
// Byte consumption stays lazy; only stdin needs a shared cursor.
return (path) => {
if (!isStdin(path, dash))
return read(path);
return sole ? input : inputStream();
};
}
/** Stat each operand on its backend, or as a stream for a stdin one. */
export function stdinStat(stat, dash = true) {
return (path) => isStdin(path, dash)
? Promise.resolve(new FileStat({ name: '-', type: FileType.FIFO }))
: stat(path);
}
/** Drive an already validated stream through bounded calls. Mirrors Python's stream_output. */
export function streamOutput(stream, source, signal) {
let started = false;
const stop = new AbortController();
const stopped = mergeSignals(signal, stop.signal);
async function* output() {
started = true;
try {
yield* streamOutputChunks(stream, source, stopped);
}
catch (error) {
if (!stop.signal.aborted ||
stopped?.reason !== stop.signal.reason ||
!(error instanceof DOMException) ||
error.name !== 'AbortError')
throw error;
}
}
const producer = output();
return new OutputStream(producer, async () => {
stop.abort();
try {
await producer.return(undefined);
}
finally {
await closeStream(stream);
if (!started)
await closeQuietly(source);
}
});
}
//# sourceMappingURL=stream.js.map