@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
173 lines • 8.28 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 { ContentType, DirListing, FileStat, FileType } from "../../types.js";
import { enoent, enotdir } from "../../errors/fs.js";
import { rstripSlash, stripSlash } from "../../utils/slash.js";
import { compareCodePoints } from "../../utils/sort.js";
import { INVALID, ROOT } from "./scope.js";
/**
* What a paged listing's pages hold, and no page past the one that brings
* `limit` rows; the pages are closed there. Mirrors Python's `held_pages`.
*/
export async function heldPages(pages, limit) {
const held = [];
for await (const page of pages) {
for (const item of page)
held.push(item);
if (limit !== undefined && held.length >= limit)
break;
}
return held;
}
/**
* A paged listing's rows, partial once they reach a walk's `limit`: its
* lister stopped paging there, so a row past them may exist. Mirrors
* Python's `cut_listing`.
*/
export function cutListing(rows, limit) {
if (limit !== undefined && rows.length >= limit)
return new DirListing({ entries: rows, partial: true });
return rows;
}
/**
* Build a hierarchy readdir: dispatch, guards, rows as stat answers.
*
* A lister fetches one directory kind and answers its children's stats;
* classification, existence guards and the shape a kind shares happen here,
* identically for every backend. Each listed row is finished from its own
* kind: `extras` add the slots it was addressed by, and a leaf the lister
* left untyped renders as its scope's filetype (JSON by default), so a listed
* row is what `stat` answers. A dot-prefixed name is dropped from the
* listing: the classifier refuses every dot-leading segment, so listing one
* would advertise a path that stat, read and child readdir all report
* absent. A lister may answer null instead of a listing: the directory's
* container does not exist, reported as ENOENT on the virtual path. A lister
* may answer a `DirListing` to seed descendant listings its fetch already
* proved, or to mark a view or a window. A walk's `limit` reaches every
* lister in `ScopeMatch.limit`.
*
* An entry lister is for a directory whose existence and contents are
* already proven by its parent's listing: it is handed the row that listing
* named (`entry`), so entering a directory a traversal just listed costs no
* call beyond its own listing. The facts a child listing needs beyond the
* API's own answers ride that row's `extra` (trello stashes each `card.json`
* size on the card's row).
*
* A parent-entry lister is for a directory whose existence is decided by its
* PARENT's row rather than its own, so the proof is the parent's row
* (`parent`) and the fetch takes the rest from the match. A kind appears in
* at most one of the three tables.
*
* `staticRoot` names fixed top-level directories, for backends whose root
* never changes. `guards` are existence checks that run before the listing,
* so a vanished container is ENOENT. `patternKinds` holds one entry per kind
* whose listing is a bounded window, the test for whether a glob is one its
* lister can move the window to (`hasGlobSpan` for a date-keyed listing): a
* glob that passes reaches the lister in `ScopeMatch.pattern`, and the mount
* serves no stored window for a glob. `leafError` is what listing a leaf
* raises; fixed hierarchies historically answer ENOENT. `extras` are
* per-kind `FileStat.extra` payloads derived from a listed row's own slots.
* Mirrors `make_readdir` in `mirage/core/hierarchy/readdir.py`.
*/
export function makeReaddir(detect, options) {
const { listers, staticRoot, guards } = options;
const patternKinds = options.patternKinds ?? {};
const entryListers = options.entryListers ?? {};
const parentEntryListers = options.parentEntryListers ?? {};
const leafError = options.leafError ?? 'enoent';
const extras = options.extras ?? {};
const overlap = [
...Object.keys(listers).filter((kind) => entryListers[kind] !== undefined || parentEntryListers[kind] !== undefined),
...Object.keys(entryListers).filter((kind) => parentEntryListers[kind] !== undefined),
];
if (overlap.length > 0) {
throw new Error(`kinds in several lister tables: ${overlap.sort(compareCodePoints).join(', ')}`);
}
const finish = (row, folder) => {
const match = detect(`${folder}/${row.name}`);
const extraFn = extras[match.kind];
const scope = match.scope;
const typed = row.type === FileType.FILE && row.content === null && scope?.leaf === true;
if (extraFn === undefined && !typed)
return row;
return row.with({
...(extraFn !== undefined ? { extra: { ...row.extra, ...extraFn(match) } } : {}),
...(typed ? { content: scope.filetype ?? ContentType.JSON } : {}),
});
};
const shown = (rows, folder) => rows.filter((row) => !row.name.startsWith('.')).map((row) => finish(row, folder));
return async function readdir(accessor, pathSpec, entry = null, parent = null, limit) {
const virtual = pathSpec.virtual;
const path = (pathSpec.pattern !== null ? pathSpec.dir : pathSpec).mountPath;
const folder = rstripSlash(path);
const detected = detect(path);
if (detected.kind === INVALID)
throw enoent(virtual);
const pushable = patternKinds[detected.kind];
const globbed = pathSpec.pattern !== null && pushable?.(pathSpec.pattern) === true;
const match = {
...(globbed ? { ...detected, pattern: pathSpec.pattern } : detected),
...(limit === undefined ? {} : { limit }),
};
if (match.kind === ROOT && staticRoot !== undefined) {
return shown(staticRoot.map((name) => new FileStat({ name, type: FileType.DIRECTORY })), folder);
}
const lister = listers[match.kind];
const entryLister = entryListers[match.kind];
const parentLister = parentEntryListers[match.kind];
if (lister === undefined && entryLister === undefined && parentLister === undefined) {
if (match.scope !== null && match.scope.leaf && leafError === 'enotdir') {
throw enotdir(virtual);
}
throw enoent(virtual);
}
const guard = guards?.[match.kind];
if (guard !== undefined)
await guard(accessor, match, virtual);
let listed;
if (entryLister !== undefined) {
if (entry === null)
throw enoent(virtual);
listed = await entryLister(accessor, match, entry);
}
else if (parentLister !== undefined) {
if (parent === null)
throw enoent(virtual);
listed = await parentLister(accessor, match, parent);
}
else if (lister !== undefined) {
const maybe = await lister(accessor, match);
if (maybe === null)
throw enoent(virtual);
listed = maybe;
}
else {
throw enoent(virtual);
}
if (!(listed instanceof DirListing))
return shown(listed, folder);
const seeds = new Map();
for (const [rel, rows] of listed.seeds) {
seeds.set(rel, shown(rows, `${folder}/${stripSlash(rel)}`));
}
return new DirListing({
entries: shown(listed.entries, folder),
seeds,
partial: listed.partial,
window: listed.window,
version: listed.version,
});
};
}
//# sourceMappingURL=readdir.js.map