@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
287 lines • 12.3 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 { isStdin, operandHeader, showsHeaders } from "../utils/stream.js";
import { recordSeparator } from "../utils/lines.js";
import { stdinStream, stdinStat } from "../utils/stream.js";
import { IOResult } from "../../../io/types.js";
import { isFsError } from "../../../errors/fs.js";
import { READ_FAILURES } from "../../../errors/constants.js";
import { FileType, Limit } from "../../../types.js";
import { CHAR_DEVICE_MAX_BYTES } from "../utils/constants.js";
import { asyncChain, closeQuietly } from "../../../io/stream.js";
import { CHUNK_SIZE, chunks } from "../../../io/cooperative.js";
import { SharedInput } from "../../../io/async_line_iterator.js";
import { YieldBudget } from "../../../io/yield_budget.js";
import { truncateStream } from "../utils/limit.js";
import { reportFsError, splitOpened } from "../utils/operands.js";
import { resolveSource } from "../utils/stream.js";
import { FlagView } from "../../spec/flag_view.js";
import {} from "../../spec/types.js";
import { specOf } from "../../spec/builtins.js";
import { concat } from "../../../utils/bytes.js";
import { encodeText } from "../../../shell/bytes.js";
import { StreamRefusal } from "../../../rs/errors.js";
import { refused, scan } from "../../../rs/scan.js";
/**
* head's options, -n and -c read by mirage-rs before any operand is resolved:
* the flags, or head's refusal of a count. Mirrors Python's `parse_flags`.
*/
export function parseFlags(bag) {
const fl = new FlagView(bag, specOf('head'));
let values;
try {
values = scan('head', fl.texts('lines', 'bytes'));
}
catch (err) {
if (!(err instanceof StreamRefusal))
throw err;
return refused(err);
}
const { lines, bytes } = values;
if ((lines !== null && typeof lines !== 'number') ||
(bytes !== null && typeof bytes !== 'number')) {
throw new Error(`head: mirage-rs read -n/-c as ${JSON.stringify(values)}`);
}
// The last of -q and -v decides, as in GNU head.
const headers = fl.typedOrder('quiet', 'silent', 'verbose').at(-1);
return {
lines: lines ?? 10,
bytesMode: bytes,
quiet: headers === 'quiet' || headers === 'silent',
verbose: headers === 'verbose',
zeroTerminated: fl.asBool('zero_terminated'),
};
}
/**
* The lines of `chunk`, at most `wanted`, and where the last of them ends,
* just past its delimiter (0 for none), counted a bounded slice at a time
* with the budget run between slices, so a large chunk still lets its run
* be stopped. Mirrors Python's `_count_lines`.
*/
async function countLines(chunk, delimiter, wanted, budget) {
let found = 0;
let end = 0;
for (let start = 0; start < chunk.byteLength; start += CHUNK_SIZE) {
const slice = chunk.subarray(start, start + CHUNK_SIZE);
for (let at = slice.indexOf(delimiter); at >= 0; at = slice.indexOf(delimiter, at + 1)) {
found += 1;
end = start + at + 1;
if (found === wanted)
return [found, end];
}
const paused = budget.run();
if (paused !== undefined)
await paused;
}
return [found, end];
}
/**
* Emit the head of a stream like GNU `head`.
*
* Bytes (`bytesMode`): positive = first N bytes, negative = all but the last N
* bytes, 0 = nothing. Lines (`lines`): positive = first N lines, negative = all
* but the last N lines, 0 = nothing. A final line without a trailing newline is
* preserved as-is (no newline is appended). head takes its input whole, writes
* the producer's chunks on, as much of each as it keeps in one piece, and may
* be called outside the factory's guard, so each pull races the abort, and a
* line count checks it and yields to timers between bounded slices.
*/
async function* headStream(source, lines, bytesMode, zeroTerminated = false, signal) {
const pulls = chunks(source, signal, false);
if (bytesMode !== null) {
if (bytesMode === 0)
return;
if (bytesMode > 0) {
let remaining = bytesMode;
for await (const chunk of pulls) {
if (chunk.byteLength >= remaining) {
if (source instanceof SharedInput && source.seekable)
source.lines.unread(chunk.subarray(remaining));
if (remaining > 0)
yield chunk.subarray(0, remaining);
return;
}
yield chunk;
remaining -= chunk.byteLength;
}
return;
}
const keep = -bytesMode;
let buf = new Uint8Array(0);
for await (const chunk of pulls) {
buf = concat([buf, chunk]);
if (buf.byteLength > keep) {
yield buf.subarray(0, buf.byteLength - keep);
buf = buf.subarray(buf.byteLength - keep);
}
}
return;
}
const delimiter = recordSeparator(zeroTerminated);
const budget = new YieldBudget(signal);
if (lines >= 0) {
if (lines === 0)
return;
let emitted = 0;
for await (const chunk of pulls) {
const [found, end] = await countLines(chunk, delimiter, lines - emitted, budget);
emitted += found;
if (emitted < lines) {
if (chunk.byteLength > 0)
yield chunk;
continue;
}
if (source instanceof SharedInput && source.seekable)
source.lines.unread(chunk.subarray(end));
yield chunk.subarray(0, end);
return;
}
return;
}
// The chunks not yet written, each with its lines: what lies before the
// last `keep` lines goes out as soon as it is known.
const keep = -lines;
const kept = [];
let next = 0;
let held = 0;
for await (const chunk of pulls) {
if (chunk.byteLength === 0)
continue;
const [count] = await countLines(chunk, delimiter, chunk.byteLength + 1, budget);
kept.push([chunk, count]);
held += count;
for (let entry = kept[next]; entry !== undefined && held > keep; entry = kept[next]) {
const [first, firstCount] = entry;
const over = held - keep;
if (firstCount < over || (firstCount === over && first.at(-1) === delimiter)) {
next += 1;
held -= firstCount;
yield first;
continue;
}
const [, end] = await countLines(first, delimiter, over, budget);
kept[next] = [first.subarray(end), firstCount - over];
held = keep;
yield first.subarray(0, end);
}
if (next * 2 > kept.length) {
kept.splice(0, next);
next = 0;
}
}
const last = kept.at(-1);
if (held === keep && last !== undefined && next < kept.length && last[0].at(-1) !== delimiter) {
// The last line lacks its separator and still counts as one, so one
// line more is not among the last.
for (const [first, count] of kept.slice(next)) {
if (count > 0) {
yield first.subarray(0, first.indexOf(delimiter) + 1);
break;
}
yield first;
}
}
}
async function* headMulti(stream, paths, lines, bytesMode, showHeaders, zeroTerminated, unread, refused, signal) {
let printed = false;
for (const p of paths) {
// A directory opened: its header prints and its read fails.
const body = unread.has(p.virtual)
? null
: headStream(stream(p), lines, bytesMode, zeroTerminated, signal);
try {
// The first pull opens the file, and `stream` names in `refused` one
// whose open it refused, which prints no header, as GNU opens a file
// before it heads it.
const first = await body?.next();
if (showHeaders && !refused.has(p.virtual)) {
yield operandHeader(p, printed);
printed = true;
}
if (body === null || first === undefined || first.done === true)
continue;
yield first.value;
for (let next = await body.next(); next.done !== true; next = await body.next()) {
yield next.value;
}
}
finally {
await closeQuietly(body);
}
}
}
/**
* Run head over resolved operands, GNU semantics. A line count (`-n N`, or
* the default 10) is asked of `firstLines` first, so a mount that can fetch
* the start of a file on its own (a `LIMIT` on a table) does not send the
* rest; null, or no `firstLines`, reads the file. Mirrors Python's
* `head_generic`; `parsed` holds the options, read before the operands were.
*/
export async function headGeneric(parsed, paths, texts, ctx, stat, stream, firstLines) {
stat = stdinStat(stat);
stream = stdinStream(stream, ctx.stdin);
if (paths.length > 0) {
const showHeaders = showsHeaders(parsed.verbose, parsed.quiet, paths.length);
const [opened, unread, err] = await splitOpened(paths, stat, 'head');
const io = new IOResult({
exitCode: err === '' ? 0 : 1,
stderr: err === '' ? null : encodeText(err),
});
if (opened.length === 0)
return [null, io];
const refused = new Set();
const pushed = parsed.bytesMode === null && !parsed.zeroTerminated && parsed.lines > 0;
const sourceFor = async function* (p) {
try {
const source = stream(p);
if ((await stat(p)).type === FileType.CHAR_DEVICE && parsed.bytesMode === null) {
yield* truncateStream(source, io, new Limit({ maxBytes: CHAR_DEVICE_MAX_BYTES }));
return;
}
if (pushed && firstLines !== undefined && !isStdin(p)) {
const lines = await firstLines(p, parsed.lines);
if (lines !== null) {
yield lines;
return;
}
}
yield* source;
}
catch (err) {
if (!isFsError(err))
throw err;
// Reported as it happens, so a reader that stops at the header still
// leaves the status; the next operand still prints. One whose open was
// refused prints no header; a read that fails once the stat passed (a
// table past its read cap) keeps it.
const code = err.code;
if (code === undefined || !READ_FAILURES.has(code))
refused.add(p.virtual);
await reportFsError(io, 'head', p, err);
}
};
return [
headMulti((path) => (isStdin(path) && ctx.stdin instanceof SharedInput ? ctx.stdin : sourceFor(path)), opened, parsed.lines, parsed.bytesMode, showHeaders, parsed.zeroTerminated, unread, refused, ctx.signal),
io,
];
}
const source = resolveSource(ctx.stdin);
const body = headStream(source, parsed.lines, parsed.bytesMode, parsed.zeroTerminated, ctx.signal);
// -v heads a stdin nobody named with the name it gives `-`.
return [
parsed.verbose && !parsed.quiet ? asyncChain([operandHeader(null), body]) : body,
new IOResult(),
];
}
//# sourceMappingURL=head.js.map