UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

183 lines • 7.34 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 { 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