@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
194 lines • 7.87 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 { IOResult, materialize } from "../../../io/types.js";
import { asyncChain } from "../../../io/stream.js";
import { FileType } from "../../../types.js";
import { eisdir, isDotWalkError, isFsError } from "../../../errors/fs.js";
import { fsErrorLine } from "../../../errors/render.js";
import { READ_FAILURES } from "../../../errors/constants.js";
import { rstripSlash } from "../../../utils/slash.js";
import { compareCodePoints } from "../../../utils/sort.js";
import { encodeText } from "../../../shell/bytes.js";
import { concat } from "../../../utils/bytes.js";
// What a stat row's `name` should say for a path: the basename, or `/`
// for the workspace root, which is the spelling `namespaceStat` already
// uses for a namespace-only directory. Mirrors Python's `operand_name`,
// which takes a PathSpec because its one caller outside this module has
// one; here both callers hold the virtual string.
export function operandName(virtual) {
const trimmed = rstripSlash(virtual);
const cut = trimmed.lastIndexOf('/');
return trimmed.slice(cut + 1) || '/';
}
/**
* Stat one operand the way a reporting command needs it.
*
* Two things no single backend stat can get right, both about paths that
* are namespace structure rather than backend state:
*
* A path that only exists because mounts or links sit under it (`/repos`
* when `/repos/alpha` is mounted) has no backend to answer for it, so the
* backend stat throws and the operand reads as absent. `statPath` routes
* through the dispatcher, which answers such a path from the namespace,
* so it is asked second and only on a miss. Its row is already named from
* the path.
*
* A mount root has a backend, but that backend names its own root rather
* than the path: ram answers `/`, and disk answers the host directory's
* basename, which leaks the path behind the mount. So the row is renamed
* here, the way `ls` renames a child-mount row for the same reason.
*
* Mirrors Python `mirage.commands.builtin.utils.operands.operand_stat`.
*/
export async function operandStat(path, stat, statPath, mounts, links) {
let row;
try {
row = await stat(path);
}
catch (e) {
// An operand that did not resolve is answered by no namespace
// structure under the path it simplifies to.
if (!isFsError(e) || isDotWalkError(e))
throw e;
if (mounts?.visibleDescendants(path.virtual).length === 0 &&
(links?.subtree(path.virtual).length ?? 0) === 0)
throw e;
const fallback = statPath === undefined || statPath === null ? null : await statPath(path);
if (fallback === null)
throw e;
return fallback;
}
if (mounts?.isRoot(path.virtual) === true) {
return row.with({ name: operandName(path.virtual) });
}
return row;
}
/**
* The mount roots a walk of `directory` reaches first, sorted: the edge of
* the directory's own filesystem, so a mount nested in a mount is not one
* of them. Mirrors Python's mount_points.
*/
export function mountPoints(mounts, directory) {
if (mounts === undefined || mounts === null)
return [];
const roots = mounts.visibleDescendants(directory);
return roots
.filter((root) => !roots.some((other) => root.startsWith(`${other}/`)))
.sort(compareCodePoints);
}
// Partition operands into readable paths and GNU stderr lines. Read-family
// commands (cat/head/tail/wc) process remaining operands after one fails,
// per GNU coreutils: each failed operand becomes one `<cmd>: <path>:
// <strerror>` line and the command exits 1 while still emitting output for
// the operands that resolved. Each path is stat'ed eagerly so a lazy output
// stream never aborts mid-drain on a missing operand. Non-filesystem errors
// keep propagating.
export async function splitReadable(paths, stat, cmdName) {
const readable = [];
let err = '';
for (const p of paths) {
let st;
try {
st = await stat(p);
}
catch (e) {
if (!isFsError(e))
throw e;
err += fsErrorLine(cmdName, p, e);
continue;
}
if (st.type === FileType.DIRECTORY) {
err += fsErrorLine(cmdName, p, eisdir(p));
continue;
}
readable.push(p);
}
return [readable, err];
}
/**
* splitReadable for the commands that head each operand. GNU head and tail
* open an operand before they read it, and a directory opens: its `==> name
* <==` header prints and only the read after it fails. So a directory keeps
* its place among the opened operands, named in the unread set, while one
* that does not open at all (a missing name) is dropped as splitReadable
* drops it. Mirrors Python's split_opened.
*/
export async function splitOpened(paths, stat, cmdName) {
const opened = [];
const unread = new Set();
let err = '';
for (const p of paths) {
let failure = null;
try {
if ((await stat(p)).type === FileType.DIRECTORY)
failure = eisdir(p);
}
catch (e) {
if (!isFsError(e))
throw e;
failure = e;
}
if (failure !== null) {
err += fsErrorLine(cmdName, p, failure);
const code = failure.code;
if (code === undefined || !READ_FAILURES.has(code))
continue;
unread.add(p.virtual);
}
opened.push(p);
}
return [opened, unread, err];
}
// IOResult carrying operand-split stderr lines: exit 1 when any operand
// failed, exit 0 otherwise.
export function operandsIo(err) {
return new IOResult({
exitCode: err === '' ? 0 : 1,
stderr: err === '' ? null : encodeText(err),
});
}
// A one-shot stream over already-materialized bytes, for feeding buffered
// operands back through a stream transformer.
// eslint-disable-next-line @typescript-eslint/require-await
export async function* singleChunk(data) {
if (data.byteLength > 0)
yield data;
}
/** Report a deferred filesystem error while allowing later operands. */
export async function* reportedStream(source, io, path, cmdName) {
try {
yield* source;
}
catch (err) {
if (!isFsError(err))
throw err;
await reportFsError(io, cmdName, path, err);
}
}
/**
* Report one operand's filesystem failure on a result whose output goes on:
* its fsErrorLine lands after the stderr already there, and the exit code
* becomes 1. Mirrors Python's report_fs_error.
*/
export async function reportFsError(io, cmdName, path, err) {
io.stderr = concat([await materialize(io.stderr), encodeText(fsErrorLine(cmdName, path, err))]);
io.exitCode = 1;
}
/** `each` over every operand in turn, or over stdin without one. Mirrors Python's per_operand. */
export function perOperand(paths, stream, stdin, io, cmdName, each) {
if (paths.length === 0)
return each(stdin ?? new Uint8Array());
return asyncChain(paths.map((p) => each(reportedStream(stream(p), io, p, cmdName))));
}
//# sourceMappingURL=operands.js.map