UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

259 lines • 10.5 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 { 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