UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

194 lines • 7.87 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 { 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