@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
121 lines • 5.44 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 { FileStat, PathSpec } from "../../../types.js";
import { isEnotdir, isMissError, isEnoent } from "../../../errors/fs.js";
import { posixNormpath } from "../../../utils/path.js";
import { resolvePathStat } from "../../../commands/builtin/utils/paths.js";
export async function statOrNull(dispatch, path) {
// A missing destination is an expected mv case (plain rename), not an
// error to surface. ENOTDIR counts as missing too: a path under a plain
// file cannot exist. Anything else (a refusal, a loop, a transport error)
// propagates, as Python's stat_or_none does: a permission error is not a
// missing file, and a link whose target refuses its stat is not dangling.
try {
const [stat] = await dispatch('stat', path);
return stat instanceof FileStat ? stat : null;
}
catch (err) {
if (isEnoent(err) || isEnotdir(err))
return null;
throw err;
}
}
// Stat one virtual path through the workspace, null when absent.
//
// Resolves through the op dispatcher rather than one backend, so a path
// under another mount answers correctly. This is what a traversal command
// asks about its own start point: a directory can be walked, a file is
// reported as itself, and null is GNU's missing-operand error.
export async function pathStat(dispatch, virtual) {
return resolvePathStat(dispatch, PathSpec.fromStrPath(virtual, undefined, '/'));
}
// The condition a diagnostic names for a path pathStat found nothing at.
// pathStat answers null for both ways a lookup fails, since an existence
// probe treats them alike, while a diagnostic names the one the stat met:
// ENOTDIR for a path under a plain file, ELOOP for one a link loop stands
// in, ENOENT for the rest. Asked only after a miss, so its round trip is on
// the failure path. Mirrors miss_condition in probe.py.
export async function missCondition(dispatch, virtual) {
try {
await dispatch('stat', PathSpec.fromStrPath(virtual, undefined, '/'));
}
catch (err) {
if (isEnotdir(err))
return 'ENOTDIR';
if (isMissError(err))
return 'ENOENT';
if (isEloop(err))
return 'ELOOP';
throw err;
}
return 'ENOENT';
}
function isEloop(err) {
return err.code === 'ELOOP';
}
// List one virtual path through the workspace, as virtual paths.
//
// Resolves through the op dispatcher rather than one backend, so a
// directory served by another mount answers. This is what a walker reads
// once it crosses a mount boundary: the subtree under a nested mount
// lives in a VFS the walker's own accessor cannot open.
export async function pathReaddir(dispatch, virtual) {
const spec = PathSpec.fromStrPath(virtual, undefined, '/');
const [entries] = await dispatch('readdir', spec);
return entries;
}
// Whether a resolved virtual path names something that exists.
export async function pathExists(dispatch, virtual) {
try {
return (await pathStat(dispatch, virtual)) !== null;
}
catch {
return false;
}
}
/**
* Where a path really points, as the dispatcher can address it: the namespace's
* walk, with a relative target's walk up from the link's own directory
* (`../a.txt`) collapsed, which the dispatcher does not do for a path it is handed
* whole. The link's directory is a real one, since the table keys every link
* by its resolved parent, so the `..` it names is that directory's parent.
* `links` is the session's link table (`Namespace.sessionLinks`). Throws
* CycleError when the chain loops. Mirrors Python's resolve_link.
*/
export function resolveLink(links, virtual) {
return posixNormpath(links.follow(virtual));
}
// The stat of what a link points at, or null when it dangles.
//
// Under -L the reported entity is the target, so its type drives -type
// and its size and mtime drive -size and -mtime. The stat goes through
// dispatch rather than one backend because a link may point into
// another mount. `links` is the session's link table
// (`Namespace.sessionLinks`). Mirrors Python's link_target_stat.
export async function linkTargetStat(links, dispatch, virtual) {
let target;
try {
target = resolveLink(links, virtual);
}
catch {
// A loop (ELOOP) is one of the two ways a link legitimately has no
// target; statOrNull maps the other (missing). Every other backend
// failure propagates, because a permission or connection error is
// not a dangling link and reporting it as one would print the link
// as -type l and exit 0.
return null;
}
return statOrNull(dispatch, PathSpec.fromStrPath(target, ''));
}
//# sourceMappingURL=probe.js.map