UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

255 lines • 10.9 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 { abortable } from "../../../../utils/abort.js"; import { discardStreams, ensureStream } from "../../../../io/stream.js"; import { chunks } from "../../../../io/cooperative.js"; import { concat as concatBytes } from "../../../../utils/bytes.js"; import { isStdin } from "../../utils/stream.js"; import { virtualKey } from "../../utils/paths.js"; import { ROOT_CWD } from "../../../constants.js"; import { OPERAND_CONCURRENCY } from "../../constants.js"; import { eisdir, isFsError } from "../../../../errors/fs.js"; import { fsErrorLine } from "../../../../errors/render.js"; import { IOResult, materialize } from "../../../../io/types.js"; import { FileType } from "../../../../types.js"; import { readFailExitCode } from "../../../spec/usage.js"; import { FlagView } from "../../../spec/flag_view.js"; import { specOf } from "../../../spec/builtins.js"; import { parseFlags as parseGrepFlags } from "../grep.js"; import { groupLine } from "../../grep_binary.js"; import { betweenFiles as rgBetweenFiles, parseFlags as parseRgFlags } from "../rg.js"; import { encodeText } from "../../../../shell/bytes.js"; // Run one native single-mount command per operand, in operand order. Each // operand executes on its owning mount through `runSingle` (which also // expands the operand's glob natively). Output is materialized and the lazy // exit code synced, so combiners see final values. /** * What sets one run's grep or rg output off from the next's. Both print a * separator between one file's context and the next file's (rg's own, or * none under --no-context-separator), and rg a blank line between --heading * groups, so the runs a line splits into join the way one run would. Nothing * for any other output, a plain line stream. */ export function runSeparator(cmdName, flagKwargs) { if (cmdName === 'rg') { return rgBetweenFiles(parseRgFlags(new FlagView(flagKwargs, specOf('rg')))); } if (cmdName === 'grep') { return groupLine(parseGrepFlags(new FlagView(flagKwargs, specOf('grep')))); } return ''; } export async function runOperands(runSingle, cmdName, scopes, texts, flagKwargs) { const results = []; for (const scope of scopes) { const [out, io] = await runSingle(cmdName, [scope], texts, flagKwargs, {}); const parts = []; try { for await (const part of chunks(out ?? new Uint8Array())) parts.push(part); } catch (e) { // A lazy stream can fail on first pull (head/tail opening the operand // mid-drain); report it like the native run would and keep the // remaining operands, GNU-style. if (!isFsError(e)) throw e; const existing = await materialize(io.stderr); const line = encodeText(fsErrorLine(cmdName, scope, e)); const merged = new Uint8Array(existing.byteLength + line.byteLength); merged.set(existing, 0); merged.set(line, existing.byteLength); io.stderr = merged; // The command's own code for a failed read, not the catch-all: a // lazy operand that fails here is the same failure the single-mount // run reports eagerly, and it must answer the same number. io.exitCode = readFailExitCode(cmdName, e); } results.push({ scope, data: concatBytes(parts), io }); } return results; } // Merge per-operand IOResults in operand order under one exit code (each // family has its own combine rule). export async function mergeOperandIos(results, exitCode) { let io = new IOResult(); for (const run of results) { io = await io.merge(run.io); } io.exitCode = exitCode; // A merge keeps the last run's records; the operands' records are wanted // together and in order, since find's actions run once over all of them // at the command boundary (`-exec {} +` is one batch across start points, // as in GNU), each under its own operand. One run without them means the // whole selection is unstructured. io.records = results.every((run) => run.io.records !== null) ? results.flatMap((run, position) => (run.io.records ?? []).map((record) => ({ ...record, run: position }))) : null; return io; } // Drop each mount prefix so a generic sees one flat namespace of full virtual // paths; the relayed primitives route each full path to its owning mount. Used // by transfer/compare where the generic does path arithmetic; read commands // pass scopes through unchanged. export function flatScopes(scopes) { return scopes.map(virtualKey); } // Minimal Context for delegating a read/compare to a generic: only flags and // stdin are read by those generics. The cross command always has path // operands, so stdin is never consulted. export function crossOpts(flagKwargs) { return { stdin: null, flags: flagKwargs, cwd: ROOT_CWD, }; } export function statOp(dispatch) { return async (p) => { const [info] = await dispatch('stat', p); return info; }; } export function readdirOp(dispatch) { return async (p, nofollow, limit) => { const [entries] = await dispatch('readdir', p, [], { ...(nofollow === true ? { nofollow } : {}), ...(limit === undefined ? {} : { limit }), }); return entries ?? []; }; } /** A directory-aware streamed reader. Mirrors Python's `file_stream_op`. */ export function fileStreamOp(dispatch) { const stat = statOp(dispatch); async function* stream(path) { if ((await stat(path)).type === FileType.DIRECTORY) throw eisdir(path); const [data] = await dispatch('read', path, [], { stream: true }); yield* ensureStream(data); } return stream; } export function streamOp(dispatch) { async function* gen(p) { const [data] = await dispatch('read', p); yield data ?? new Uint8Array(); } return gen; } /** Write one whole file on the mount that owns it. Mirrors Python's `_relay_write`. */ export async function relayWrite(dispatch, path, data) { await dispatch('write', path, [data]); } /** * Copy one file store to store, from its mount onto `dst`'s. The dispatcher * judges the source as a read and the destination as a create, and answers * EXDEV when the destination's store cannot copy from the source's. Mirrors * Python's `_relay_copy`. */ export async function relayCopy(dispatch, src, dst) { await dispatch('copy', src, [dst]); } export function transferLinksOf(links, dispatch, cwd, owns) { const relayStat = statOp(dispatch); return { links, dispatch, cwd, ...(owns !== undefined ? { owns } : {}), relay: { read: fileStreamOp(dispatch), write: (p, data) => relayWrite(dispatch, p, data), mkdir: async (p) => { await dispatch('mkdir', p); }, readdir: readdirOp(dispatch), copy: (src, dst) => relayCopy(dispatch, src, dst), }, relayStat, }; } /** Stream independent native reads in order, keeping at most four invocations open. */ export function streamOperands(runSingle, cmdName, scopes, texts, bag, separator = '') { const io = new IOResult(); async function* stream() { const controller = new AbortController(); const pending = []; let next = 0; let printed = false; const start = () => { const scope = scopes[next++]; if (scope === undefined) return; const result = runSingle(cmdName, [scope], texts, bag, { signal: controller.signal }); // Attach a rejection observer immediately while earlier operands drain. void result.catch(() => undefined); const item = { scope, result }; void result.then((ready) => { item.ready = ready; }, () => undefined); pending.push(item); }; const concurrency = scopes.some((p) => isStdin(p)) ? 1 : OPERAND_CONCURRENCY; for (let i = 0; i < concurrency; i++) start(); try { while (pending.length > 0) { const head = pending[0]; if (head === undefined) break; const { scope, result } = head; const [out, branch] = await abortable(result, controller.signal); let first = true; try { for await (const data of chunks(out ?? new Uint8Array(), controller.signal)) { if (data.byteLength === 0) continue; if (first && printed && separator !== '') yield encodeText(separator); first = false; printed = true; yield data; } } catch (err) { if (!isFsError(err)) throw err; branch.stderr = concatBytes([ await materialize(branch.stderr), encodeText(fsErrorLine(cmdName, scope, err)), ]); branch.exitCode = readFailExitCode(cmdName, err); } io.stderr = concatBytes([await materialize(io.stderr), await materialize(branch.stderr)]); io.exitCode = Math.max(io.exitCode, branch.exitCode); if (branch.refusal !== null) io.refusal = branch.refusal; pending.shift(); start(); } } finally { controller.abort(); for (const item of pending) { if (item.ready !== undefined) await discardStreams(item.ready[0], item.ready[1].stderr); else void item.result.then(([out, branch]) => discardStreams(out, branch.stderr), () => undefined); } } } return [stream(), io]; } //# sourceMappingURL=utils.js.map