@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
259 lines • 10.5 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 { activeCacheManager } from "../../cache/context.js";
import { isEntryError } from "../../errors/classify.js";
import { isEacces, isEnoent } from "../../errors/fs.js";
import { FileType, PathSpec } from "../../types.js";
import { rstripSlash, stripSlash } from "../../utils/slash.js";
import { compareCodePoints } from "../../utils/sort.js";
import { DIR_SIZE } from "../../utils/stat_view.js";
import { followedStat } from "../../view/namespace_view.js";
import { bindTree, dropPruned, keep, argsToTree, printfKind, prunedKeys, settlePrunes, treeHasAction, treeHasEmpty, treeHasPrune, } from "./find_eval.js";
import {} from "./types.js";
export function modifiedTs(modified) {
// Naive timestamps are UTC, mirroring the Python implementation.
if (modified === null || modified === '')
return null;
const hasTz = /(?:Z|[+-]\d{2}:?\d{2})$/.test(modified);
const iso = hasTz ? modified : modified.includes(':') ? `${modified}Z` : `${modified}T00:00:00Z`;
const ts = Date.parse(iso) / 1000;
return Number.isNaN(ts) ? null : ts;
}
async function statEntry(deps, path) {
const spec = new PathSpec({
virtual: path,
directory: path,
resolved: false,
vfsPath: stripSlash(path),
});
if (deps.unstatted?.has(path) === true)
return null;
try {
const row = await deps.stat(spec);
const size = row.type === FileType.FILE && row.size === null
? (await activeCacheManager()?.cachedSizes([spec]))?.[0]
: null;
return size == null ? row : row.with({ size });
}
catch (err) {
// Missing entries resolve to null. Any other failure does too when the
// caller collects it; otherwise it (a rate limit, an auth failure)
// propagates.
if (isEnoent(err))
return null;
if (deps.unstatted === undefined || !isEntryError(err))
throw err;
deps.unstatted.set(path, err);
return null;
}
}
async function isEmptyEntry(deps, path, isDir) {
if (isDir) {
const spec = new PathSpec({
virtual: path,
directory: path,
resolved: false,
vfsPath: stripSlash(path),
});
try {
return (await deps.readdir(spec)).length === 0;
}
catch (err) {
if (isEnoent(err))
return false;
if (isEacces(err) && deps.unreadable !== undefined) {
if (!deps.unreadable.includes(path))
deps.unreadable.push(path);
return false;
}
throw err;
}
}
const st = await statEntry(deps, path);
return st !== null && st.type === FileType.FILE && st.size === 0;
}
async function walkCollect(deps, spec, maxDepth, depth, out, chain = []) {
if (maxDepth !== null && depth > maxDepth)
return;
let children;
try {
children = await deps.readdir(spec);
}
catch (err) {
if (isEnoent(err))
return;
if (isEacces(err) && deps.unreadable !== undefined) {
deps.unreadable.push(spec.virtual);
return;
}
throw err;
}
for (const child of children) {
// Classification is stat's job (an index lookup right after the readdir
// that populated it). The one in-band proof is a trailing slash on a
// cold listing: no backend renders a file with one. Name heuristics
// beyond that guessed wrong (attachments and uploads carry whatever
// name the sender gave them) and are gone.
const trimmed = child.endsWith('/') ? rstripSlash(child) : child;
let kind = 'd';
if (!child.endsWith('/')) {
const s = await statEntry(deps, trimmed);
kind = s === null ? 'f' : printfKind(s);
}
const isFolder = kind === 'd';
let below = chain;
if (isFolder && deps.follow != null) {
// Under -L a directory is the one it resolves to, and one already open
// above is GNU's file system loop: never a row, never entered.
const here = deps.follow.resolve(trimmed);
const ancestor = chain.find(([, real]) => real === here);
if (ancestor !== undefined) {
deps.loops?.push([trimmed, ancestor[0]]);
continue;
}
below = [...chain, [trimmed, here]];
}
out.push({ path: trimmed, depth, file: !isFolder, kind });
if (!isFolder)
continue;
const childSpec = new PathSpec({
virtual: trimmed,
directory: trimmed,
resolved: false,
vfsPath: stripSlash(trimmed),
});
await walkCollect(deps, childSpec, maxDepth, depth + 1, out, below);
}
}
export async function walkFind(path, found, options = {}) {
const links = found.follow;
const deps = {
...found,
stat: (spec) => followedStat((p) => found.stat(p), links, spec),
};
const collected = [];
// GNU lists the search root itself at depth 0 (even for the mount
// root), so `-maxdepth 0` prints just the root and `-name` can match
// the root's own basename.
const rootPath = path.virtual !== '/' ? rstripSlash(path.virtual) : '/';
const pruneTree = bindTree(argsToTree(options), '', path.virtual, path.rawPath);
const prunes = treeHasPrune(pruneTree);
const needEmpty = treeHasEmpty(pruneTree);
const acts = treeHasAction(pruneTree);
const readDirectory = async (spec) => {
if (deps.enters !== undefined && !deps.enters(spec.virtual))
return [];
if (prunes) {
const key = rstripSlash(spec.virtual) || '/';
const depth = rstripSlash(spec.virtual).split('/').length - rstripSlash(rootPath).split('/').length;
const info = await statEntry(deps, spec.virtual);
const isEmpty = needEmpty ? await isEmptyEntry(deps, spec.virtual, true) : null;
keep({
key,
name: rstripSlash(spec.virtual).split('/').pop() ?? '',
kind: 'd',
depth,
isEmpty,
mtime: info === null ? null : modifiedTs(info.modified),
}, pruneTree, options.minDepth, acts);
if (prunedKeys(pruneTree).includes(key))
return [];
}
return (deps.listing ?? deps.readdir)(spec);
};
let rootStat = null;
try {
rootStat = await deps.stat(path);
}
catch (err) {
if (!isEnoent(err))
throw err;
}
if (rootStat !== null) {
collected.push({
path: rootPath,
depth: 0,
file: rootStat.type !== FileType.DIRECTORY,
kind: printfKind(rootStat),
});
}
// GNU depth convention: the search root is depth 0, its children are
// depth 1, so the walk starts at 1 and -maxdepth 0 descends nowhere. A
// start point that is not a directory has no children, so readdir on it
// is either an error the walk would have to swallow (Box answers
// ENOTDIR) or a wasted round trip everywhere else.
if (rootStat === null || rootStat.type === FileType.DIRECTORY) {
await walkCollect({ ...deps, readdir: readDirectory }, path, options.maxDepth ?? null, 1, collected, links != null ? [[rootPath, links.resolve(rootPath)]] : []);
}
const results = [];
const tree = bindTree(argsToTree(options), '', path.virtual, path.rawPath);
const needSize = options.minSize != null || options.maxSize != null;
const needMtime = options.mtimeMin != null || options.mtimeMax != null;
const learned = new Map();
collected.sort((a, b) => compareCodePoints(a.path, b.path));
for (const entry of collected) {
const name = entry.path.split('/').pop() ?? '';
const key = entry.path === '' ? '/' : entry.path;
let isEmpty = null;
if (needEmpty) {
isEmpty = await isEmptyEntry(deps, entry.path, !entry.file);
}
// With a time test in the tree the stat comes first, so the entry
// answers the test itself and a -prune after it fires only where GNU's
// would; the stat that -size alone needs waits for the rows the tree
// kept.
let st = null;
if (needMtime) {
st = await statEntry(deps, entry.path);
if (st === null)
continue;
learned.set(key, modifiedTs(st.modified));
}
const findEntry = {
key,
name,
kind: entry.kind,
depth: entry.depth,
isEmpty,
mtime: learned.get(key) ?? null,
};
if (!keep(findEntry, tree, options.minDepth, acts))
continue;
if (needSize && entry.file && st === null) {
st = await statEntry(deps, entry.path);
if (st === null)
continue;
}
if (needSize) {
const size = entry.file ? (st?.size ?? 0) : DIR_SIZE;
if (options.minSize != null && size < options.minSize)
continue;
if (options.maxSize != null && size > options.maxSize)
continue;
}
if (needMtime && st !== null) {
const mt = modifiedTs(st.modified);
if (mt === null)
continue;
if (options.mtimeMin != null && mt < options.mtimeMin)
continue;
if (options.mtimeMax != null && mt > options.mtimeMax)
continue;
}
results.push(key);
}
settlePrunes(tree, learned);
return dropPruned(results, tree);
}
//# sourceMappingURL=find.js.map