@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
183 lines • 7.34 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 { lstripSlash, rstripSlash, stripSlash } from "../utils/slash.js";
import { PathSpec } from "../types.js";
import { QUOTED_CHARS } from "./fnmatch.js";
/**
* Normalize a key prefix, the one rule every object-key backend applies on
* both hosts: strip leading slashes and ensure one trailing slash. A prefix
* that is empty once its leading slashes are gone ('', '/') is no prefix at
* all, so a root-spelled prefix never puts a slash in front of every key.
* Mirrors Python `normalize` (`utils/key_prefix.py`).
*/
export function normalize(raw) {
const stripped = lstripSlash(raw ?? '');
if (stripped === '')
return '';
return stripped.endsWith('/') ? stripped : `${stripped}/`;
}
/** Prepend a normalized prefix to a virtual path. */
export function apply(prefix, path) {
return prefix + lstripSlash(path);
}
/** Same as apply() but guarantees a trailing slash for LIST-style ops. */
export function applyDir(prefix, path) {
const key = apply(prefix, path);
if (key === '' || key.endsWith('/'))
return key;
return `${key}/`;
}
/** Strip a normalized prefix from a backend-returned key. */
export function strip(prefix, key) {
if (prefix !== '' && key.startsWith(prefix))
return key.slice(prefix.length);
return key;
}
/**
* Remove a mount prefix from a virtual path at a path boundary.
*
* A sibling that only shares the prefix as a string (`/database` vs a
* `/data` prefix) is left untouched.
*
* Example:
* stripMount('/data/sub/x.txt', '/data') -> '/sub/x.txt'
* stripMount('/database/x.txt', '/data') -> '/database/x.txt'
* stripMount('/data', '/data') -> '/'
* stripMount('/x.txt', '') -> '/x.txt'
*
* A glob word's quote marks (`glob_walk.markGlobs`) read as the characters
* they stand for, one for one: a quoted prefix still names its mount
* (`'/team+'/*`), and the rest keeps its marks.
*/
export function stripMount(virtual, prefix) {
const head = virtual.slice(0, prefix.length);
if (prefix !== '' &&
(head === prefix || Array.from(head, (c) => QUOTED_CHARS[c] ?? c).join('') === prefix)) {
const rest = virtual.slice(prefix.length);
if (prefix.endsWith('/') || rest === '' || rest.startsWith('/')) {
return rest === '' ? '/' : rest;
}
}
return virtual;
}
/**
* Whether `candidate` is `root` itself or sits below it.
*
* A boundary-aware prefix test, so a sibling that merely shares the string
* (`/data/xy` against `/data/x`) is not counted. Both sides are compared
* without a trailing slash, because a backend may have keyed a directory
* either way. Mirrors Python `under_path` (`utils/key_prefix.py`).
*
* Example:
* underPath('/data/x/y', '/data/x') -> true
* underPath('/data/x', '/data/x/') -> true
* underPath('/data/xy', '/data/x') -> false
*/
export function underPath(candidate, root) {
const base = rstripSlash(root);
if (base === '')
return true;
return rstripSlash(candidate) === base || candidate.startsWith(`${base}/`);
}
/**
* Backend key for a virtual path under a mount prefix.
*
* Example:
* mountKey('/data/sub/x.txt', '/data') -> 'sub/x.txt'
* mountKey('/data', '/data') -> ''
* mountKey('/x.txt', '') -> 'x.txt'
*/
export function mountKey(virtual, prefix) {
return stripSlash(stripMount(virtual, prefix));
}
/**
* Backend key for a child virtual path, derived from its parent.
*
* A child shares the parent's mount prefix, so its key is the child
* virtual path with the same prefix removed. The prefix length is
* recovered from the parent's `virtual`/`vfsPath` pair, so no mount
* context is needed.
*
* Example:
* rekey('/data/sub', 'sub', '/data/sub/x.txt') -> 'sub/x.txt'
* rekey('/data', '', '/data/x.txt') -> 'x.txt'
*/
export function rekey(parentVirtual, parentVfsPath, child) {
const prefixLen = rstripSlash(parentVirtual).length - parentVfsPath.length;
return stripSlash(child.slice(prefixLen));
}
/**
* Recover a mount prefix from a virtual path and its backend key.
*
* The inverse of stamping: given a path's virtual form and the key the
* mount stamped, return the mount prefix that was stripped off.
*
* Example:
* mountPrefixOf('/data/sub', 'sub') -> '/data'
* mountPrefixOf('/data', '') -> '/data'
* mountPrefixOf('/x.txt', 'x.txt') -> ''
*/
export function mountPrefixOf(virtual, vfsPath) {
const prefixLen = rstripSlash(virtual).length - vfsPath.length;
return rstripSlash(virtual.slice(0, prefixLen));
}
/**
* The path below the mount prefix, spelled as the virtual path is: `/` for
* the mount root, else the slash-led remainder with any trailing slash kept.
* Mirrors Python's `raw_path_of`.
*/
export function rawPathOf(path) {
const prefix = mountPrefixOf(path.virtual, path.vfsPath);
return prefix !== '' && path.virtual.startsWith(prefix)
? path.virtual.slice(prefix.length) || '/'
: path.virtual;
}
// A PathSpec for a mount-local key, addressed like `root`. Rebuilds the
// virtual path from the mount prefix `root` sits behind, so a backend holding
// only a key (an ancestor it walked to, say) can name it the way the user
// would see it. Mirrors Python's mounted_path.
export function mountedPath(root, mountPath) {
const prefix = mountPrefixOf(root.virtual, root.vfsPath);
const virtual = prefix !== '' ? prefix + mountPath : mountPath;
return PathSpec.fromStrPath(virtual, stripSlash(mountPath));
}
/** The path a backend key under `keyPrefix` names, addressed like `root`. Mirrors Python `key_path`. */
export function keyPath(root, keyPrefix, key) {
return mountedPath(root, `/${strip(keyPrefix, key).replace(/^\/+/, '')}`);
}
// `path` spelled as `rawPath`, every other field kept: Python's
// `dataclasses.replace(path, raw_path=...)`.
export function respelled(path, rawPath) {
return new PathSpec({
virtual: path.virtual,
directory: path.directory,
vfsPath: path.vfsPath,
pattern: path.pattern,
resolved: path.resolved,
rawPath,
dotted: path.dotted,
walkError: path.walkError,
});
}
/** The path of `name` inside the directory `spec`, on its mount. Mirrors Python's `child_spec`. */
export function childSpec(spec, name) {
const base = rstripSlash(spec.virtual);
const key = rstripSlash(spec.vfsPath);
return new PathSpec({
virtual: `${base}/${name}`,
directory: spec.virtual,
vfsPath: key === '' ? name : `${key}/${name}`,
});
}
//# sourceMappingURL=key_prefix.js.map