@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
295 lines • 14.4 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 { combinedExit } from "./fanout/exit.js";
import { joinRuns } from "./fanout/fanout.js";
import { ownedScopes } from "./scopes.js";
import { mergeOperandIos, runOperands, runSeparator } from "./utils.js";
import { filenameMode, lineWarnings, parseFlags } from "../grep.js";
import { labelFlags, parseFlags as parseRgFlags, walkFilter, filtersFiles, haystacks, headed, ignoreRules, jsonSummary, runsParallel, sortHaystacks, } from "../rg.js";
import { patternArg, compilePattern } from "../../grep_pattern.js";
import { dirAdmitted, fileAdmitted } from "../../grep_select.js";
import { isStdin, resolveSource } from "../../utils/stream.js";
import { BINARY_EXTENSIONS, getExtension } from "../../../../utils/filetype.js";
import { specOf } from "../../../spec/builtins.js";
import { FlagView, flagOccurrences } from "../../../spec/flag_view.js";
import { IOResult, materialize } from "../../../../io/types.js";
import { FileType, PathSpec } from "../../../../types.js";
import { WalkBudget } from "../../utils/tree.js";
import { rstripSlash } from "../../../../utils/slash.js";
import { walkErrorLine } from "../../rg_scan.js";
import { Totals, splitFooter } from "../../rg_search.js";
import { formatTotals, openRg } from "../../rg_stream.js";
import { formatRecords } from "../../utils/output.js";
import { concat } from "../../../../utils/bytes.js";
import { LinkResolver, linkView, mountView } from "../../utils/links.js";
import { fsErrorLine } from "../../../../errors/render.js";
import { byteView, encodeText } from "../../../../shell/bytes.js";
function admitGrep(flags, path, stat) {
if (stat.type === FileType.DIRECTORY)
return dirAdmitted(path.virtual, flags.filters);
return (stat.type === FileType.FILE &&
fileAdmitted(path.virtual, flags.filters) &&
(flags.filters.text || !BINARY_EXTENSIONS.has(getExtension(path.virtual) ?? '')));
}
/** Compose registered search handlers in operand order. Global walk modes
* use its metadata walker, then delegate each selected file. Content
* always stays with the registered handler. */
export async function runSearch(name, paths, texts, bag, dispatch, runSingle, cwd, vfs, stdin = null, utf8 = false, signal) {
const started = performance.now();
const view = new FlagView(bag, specOf(name));
const grep = name === 'grep' ? parseFlags(view) : null;
const rg = name === 'rg' ? parseRgFlags(view) : null;
// --ignore-file's rules judge every walked file, the ones a nested mount
// turns into operands of their own runs too; each run reads them again, so
// a file that could not be read is noted once, here.
const notes = [];
const ignores = rg === null ? null : await ignoreRules(view, dispatch, notes);
const nested = paths.some((p) => (mountView(vfs)?.descendants(p.virtual).length ?? 0) > 0);
const linked = paths.some((p) => (linkView(vfs)?.subtree(p.virtual).length ?? 0) > 0);
const planned = rg !== null &&
(linked ||
rg.sort !== null ||
(nested &&
(rg.maxDepth !== null ||
rg.follow ||
rg.oneFileSystem ||
filtersFiles(rg) ||
ignores?.empty === false)));
if (rg?.typeList)
return runSingle(name, paths.slice(0, 1), [...texts], bag);
let matcher = null;
if (grep !== null) {
const pattern = patternArg(texts, view);
if (pattern !== null)
matcher = compilePattern(byteView(pattern, utf8), grep.ignoreCase, grep.fixedString, grep.wholeWord, grep.syntax, utf8, grep.lineRegexp);
}
if (rg !== null && !rg.listFiles) {
const pattern = patternArg(texts, view);
if (pattern !== null) {
const program = await openRg(pattern, false, rg);
program.stream.close();
}
}
const quiet = grep?.quiet ?? rg?.quiet ?? false;
// -q stops at the first match, but --stats and --json count every file.
const stops = quiet && !(rg !== null && (rg.stats === true || rg.json === true));
const input = resolveSource(stdin);
const execute = (cmd, operands, words, bag) => runSingle(cmd, operands, words, bag, { stdin: operands.some((p) => isStdin(p)) ? input : null });
const owners = paths.map((p) => mountView(vfs)?.rootOf(p.virtual));
const walk = rg === null ? null : walkFilter(rg, undefined, ignores ?? undefined);
const admit = (path, stat) => {
if (grep !== null)
return admitGrep(grep, path, stat);
if (walk === null)
throw new Error(`No traversal filter for ${name}`);
const base = path.virtual.split('/').at(-1) ?? '';
if (stat.type === FileType.DIRECTORY)
return walk.admits(path.virtual, base, true);
return stat.type === FileType.FILE && walk.admitsFile(path.virtual, base, stat);
};
const readdir = async (virtual) => {
const [data] = await dispatch('readdir', PathSpec.fromStrPath(virtual));
return data;
};
const stat = async (virtual) => {
const [data] = await dispatch('stat', PathSpec.fromStrPath(virtual), [], { nofollow: true });
return data;
};
const root = paths[0] ?? PathSpec.fromStrPath(cwd);
async function* scopes() {
if (planned && walk !== null) {
const warnings = [];
const stopped = new Set();
const mounts = mountView(vfs) ?? null;
const budgets = new Map(paths.map((p) => [p.virtual, new WalkBudget(p, mounts)]));
const reported = new Set();
const listing = rg.listFiles;
// The plan hands each file over alone, which no search narrows, so
// each file counts here against the mount serving it; a listing reads
// none.
const charged = (h) => {
if (h.spec !== null || listing)
return true;
const top = paths.find((p) => h.virtual.startsWith(`${rstripSlash(p.virtual)}/`));
const budget = top === undefined ? undefined : budgets.get(top.virtual);
if (top === undefined || budget === undefined || budget.spend(h.virtual))
return true;
if (!budget.opens(top.virtual))
stopped.add(top.virtual);
for (const line of budget.stopped) {
if (reported.has(line))
continue;
reported.add(line);
warnings.push(`rg: ${line}`);
}
return false;
};
const links = linkView(vfs);
const resolver = links === undefined ? null : new LinkResolver(links, dispatch, cwd);
const found = haystacks(paths, readdir, stat, cwd, walk, rg, warnings, rg.oneFileSystem ? (mounts ?? null) : null, resolver, stopped);
if (rg.sort !== null && rg.sort !== 'none' && !(rg.sort === 'path' && !rg.sortReverse)) {
const listed = [];
for await (const h of found)
listed.push(h);
const kept = sortHaystacks(listed, rg).filter(charged);
for (const message of warnings)
yield { path: root, walked: false, diagnostic: message + '\n' };
warnings.length = 0;
for (const h of kept)
yield {
path: h.spec ??
new PathSpec({
virtual: h.virtual,
directory: PathSpec.fromStrPath(h.virtual).directory,
vfsPath: PathSpec.fromStrPath(h.virtual).vfsPath,
rawPath: h.shown,
}),
walked: h.spec === null,
...(h.stat === null ? {} : { stat: h.stat }),
};
return;
}
for await (const h of found) {
const admitted = charged(h);
for (const message of warnings)
yield { path: root, walked: false, diagnostic: message + '\n' };
warnings.length = 0;
if (!admitted)
continue;
yield {
path: h.spec ??
new PathSpec({
virtual: h.virtual,
directory: PathSpec.fromStrPath(h.virtual).directory,
vfsPath: PathSpec.fromStrPath(h.virtual).vfsPath,
rawPath: h.shown,
}),
walked: h.spec === null,
...(h.stat === null ? {} : { stat: h.stat }),
};
}
for (const message of warnings)
yield { path: root, walked: false, diagnostic: message + '\n' };
return;
}
for (const path of paths) {
if (rg !== null || grep?.recursive)
yield* ownedScopes(path, dispatch, vfs, admit);
else
yield { path, walked: false };
}
}
const results = [];
const walked = new Set();
for await (const scope of scopes()) {
signal?.throwIfAborted();
if (scope.error !== undefined || scope.diagnostic !== undefined) {
const message = scope.diagnostic ??
(scope.error === undefined
? ''
: rg === null
? fsErrorLine(name, scope.path, scope.error)
: walkErrorLine(scope.path.rawPath, scope.error, rg.threads !== 1 && (paths.length > 1 || scope.walked)) + '\n');
results.push({
scope: scope.path,
data: new Uint8Array(),
io: new IOResult({
exitCode: 2,
stderr: rg?.noMessages ? null : encodeText(message),
}),
});
continue;
}
const local = grep === null ? labelFlags(bag) : { ...bag };
if (grep !== null)
flagOccurrences(local).push(...flagOccurrences(bag));
if (grep !== null && filenameMode(view) === null) {
const owner = mountView(vfs)?.rootOf(scope.path.virtual);
const repeated = owners.filter((o) => o === owner).length > 1;
if (scope.stat?.type !== FileType.DIRECTORY ||
repeated ||
(scope.walked && !mountView(vfs)?.isRoot(scope.path.virtual)))
local.with_filename = true;
}
const runs = await runOperands(execute, name, [scope.path], [...texts], local);
if (scope.stat?.type === FileType.DIRECTORY)
walked.add(results.length);
for (const item of runs)
results.push(item);
if (stops && runs.at(-1)?.io.exitCode === 0)
break;
}
const code = combinedExit(name, results.map((r) => r.io.exitCode), results.map((r) => r.io.exitCode !== 0 && r.io.stderr !== null), quiet);
const [parts, footer] = rg !== null && (rg.stats === true || rg.json === true)
? await oneFooter(results, rg, walked, started)
: [results.map((r) => r.data), new Uint8Array()];
const body = concat([joinRuns(parts, runSeparator(name, bag)), footer]);
// Each run says what the line says once, grep's warnings or rg's notes of
// the ignore files it could not read; the line keeps one.
const noted = grep !== null
? encodeText(lineWarnings(view, matcher))
: rg !== null && notes.length > 0 && !rg.noMessages
? formatRecords(notes)
: new Uint8Array();
if (noted.length === 0)
return [body, await mergeOperandIos(results, code)];
for (const r of results) {
if (r.io.stderr === null)
continue;
const err = await materialize(r.io.stderr);
const kept = err.length >= noted.length && noted.every((byte, i) => err[i] === byte)
? err.subarray(noted.length)
: err;
r.io.stderr = kept.length === 0 ? null : kept;
}
const io = await mergeOperandIos(results, code);
io.stderr = concat([noted, io.stderr === null ? new Uint8Array() : await materialize(io.stderr)]);
return [body, io];
}
/**
* The runs' outputs with each --stats block or --json summary cut off, and
* the one footer that totals them, as a single rg prints it. A run that walked
* a directory on ripgrep's parallel searcher closed its last --heading group
* with a blank line too; that line goes with its footer. Mirrors Python's
* `_one_footer`.
*/
async function oneFooter(results, f, walked, started) {
const summary = jsonSummary(f);
const blank = !summary && headed(f) && runsParallel(f);
const parts = results.map((r) => r.data);
const totals = new Totals();
let found = false;
for (const [i, data] of parts.entries()) {
const [head, part] = splitFooter(data, summary);
if (part === null)
continue;
found = true;
totals.merge(part);
parts[i] = blank && head.length > 0 && walked.has(i) ? head.subarray(0, -1) : head;
}
if (!found)
return [parts, new Uint8Array()];
const total = (performance.now() - started) / 1000;
if (summary)
return [parts, await formatTotals(totals, 'summary', total)];
const printed = parts.some((d) => d.length > 0);
return [
parts,
concat([
blank && printed ? encodeText('\n') : new Uint8Array(),
await formatTotals(totals, 'stats', total),
]),
];
}
//# sourceMappingURL=search.js.map