UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

765 lines • 46.2 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. ========= var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; import { NOOPAccessor } from "../accessor/base.js"; import { enotsup } from "../errors/fs.js"; import { closeQuietly } from "../io/stream.js"; import { CapacityState, ListingVersion } from "../types.js"; import { methodName, vfsCall } from "./call.js"; import { WALK_LIMIT } from "./constants.js"; import { redactConfigWithSchema } from "./secrets.js"; import { Effect, Target } from "./types.js"; // The accessor a driver that brings none runs over: the default Python's // `BaseVFS.accessor` class attribute carries too, so a mount and a caller // never branch on its absence. const NO_ACCESSOR = new NOOPAccessor(); /** * The brand every driver carries, keyed through the symbol registry so a * script file that loaded its own copy of this package still passes the * loader's check: the class identity may differ, the brand does not. */ export const VFS_BRAND = Symbol.for('mirage.BaseVFS'); /** * A backend: an accessor, the facts about it, and its functions. * * A VFS answers `readdir`, `read` and `stat`; every other function * (`write`, `unlink`, a native `find` or `search`, ...) is optional, and a VFS answers exactly the ones it defines. Every generic * shell command (`ls`, `cat`, `grep`, `find`, `head`, `wc`, ...) runs on * the three required ones, and a line that needs a function the VFS does * not define answers `Operation not supported` at that call, so `gzip -c` * and `tar -t` still run as readers on a read-only backend. A method * marked `@vfsCall` is also reachable by name through the dispatcher * (`ws.dispatch('search_abc', path)`), with every check the dispatcher runs; the * built-in ones are marked here, and an override keeps the mark. * * Everything a tree needs to run one (the placement, the index store, the * registered commands, the reference it was built from) lives on the * mount, so an author never sees it. * * Snapshots and versions see one of two things, and a subclass picks * which by what it owns. Content the VFS holds itself (an in-memory * store) is mirage-owned state: override `getState` and `loadState` to * carry it, register the class under its name, and a snapshot or a * version rebuilds the mount with that content and no override. Content * that lives in a remote service is only observed: keep the default * state, set `supportsSnapshot` and fill `FileStat.fingerprint`, and a * snapshot pins what it read while `Workspace.load` asks for the live VFS * back. Mirrors Python's `BaseVFS`. */ let BaseVFS = (() => { let _instanceExtraInitializers = []; let _readdir_decorators; let _read_decorators; let _stat_decorators; let _firstLines_decorators; let _lastLines_decorators; let _lineCount_decorators; let _checksum_decorators; let _follow_decorators; let _readTree_decorators; let _write_decorators; let _append_decorators; let _pwrite_decorators; let _create_decorators; let _mkdir_decorators; let _unlink_decorators; let _rmdir_decorators; let _rmR_decorators; let _rename_decorators; let _copy_decorators; let _dirCopy_decorators; let _truncate_decorators; let _setattr_decorators; let _search_decorators; let _filesContaining_decorators; let _linesContaining_decorators; let _beforeFullWalk_decorators; return class BaseVFS { static { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; _readdir_decorators = [vfsCall({ effect: Effect.READ, target: Target.DIR })]; _read_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })]; _stat_decorators = [vfsCall({ effect: Effect.METADATA })]; _firstLines_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })]; _lastLines_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })]; _lineCount_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })]; _checksum_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })]; _follow_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })]; _readTree_decorators = [vfsCall({ effect: Effect.READ, target: Target.DIR, subtree: true })]; _write_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })]; _append_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })]; _pwrite_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })]; _create_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })]; _mkdir_decorators = [vfsCall({ effect: Effect.CREATE, target: Target.DIR })]; _unlink_decorators = [vfsCall({ effect: Effect.REMOVE, target: Target.FILE })]; _rmdir_decorators = [vfsCall({ effect: Effect.REMOVE, target: Target.DIR })]; _rmR_decorators = [vfsCall({ effect: Effect.REMOVE, target: Target.DIR, subtree: true })]; _rename_decorators = [vfsCall({ effect: Effect.RENAME, subtree: true })]; _copy_decorators = [vfsCall({ effect: Effect.COPY, target: Target.FILE })]; _dirCopy_decorators = [vfsCall({ effect: Effect.COPY, target: Target.DIR, subtree: true })]; _truncate_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })]; _setattr_decorators = [vfsCall({ effect: Effect.ATTR })]; _search_decorators = [vfsCall({ effect: Effect.READ, subtree: true })]; _filesContaining_decorators = [vfsCall({ effect: Effect.READ, target: Target.DIR, subtree: true })]; _linesContaining_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })]; _beforeFullWalk_decorators = [vfsCall({ effect: Effect.READ, target: Target.DIR })]; __esDecorate(this, null, _readdir_decorators, { kind: "method", name: "readdir", static: false, private: false, access: { has: obj => "readdir" in obj, get: obj => obj.readdir }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _read_decorators, { kind: "method", name: "read", static: false, private: false, access: { has: obj => "read" in obj, get: obj => obj.read }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _stat_decorators, { kind: "method", name: "stat", static: false, private: false, access: { has: obj => "stat" in obj, get: obj => obj.stat }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _firstLines_decorators, { kind: "method", name: "firstLines", static: false, private: false, access: { has: obj => "firstLines" in obj, get: obj => obj.firstLines }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _lastLines_decorators, { kind: "method", name: "lastLines", static: false, private: false, access: { has: obj => "lastLines" in obj, get: obj => obj.lastLines }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _lineCount_decorators, { kind: "method", name: "lineCount", static: false, private: false, access: { has: obj => "lineCount" in obj, get: obj => obj.lineCount }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _checksum_decorators, { kind: "method", name: "checksum", static: false, private: false, access: { has: obj => "checksum" in obj, get: obj => obj.checksum }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _follow_decorators, { kind: "method", name: "follow", static: false, private: false, access: { has: obj => "follow" in obj, get: obj => obj.follow }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _readTree_decorators, { kind: "method", name: "readTree", static: false, private: false, access: { has: obj => "readTree" in obj, get: obj => obj.readTree }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _write_decorators, { kind: "method", name: "write", static: false, private: false, access: { has: obj => "write" in obj, get: obj => obj.write }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _append_decorators, { kind: "method", name: "append", static: false, private: false, access: { has: obj => "append" in obj, get: obj => obj.append }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _pwrite_decorators, { kind: "method", name: "pwrite", static: false, private: false, access: { has: obj => "pwrite" in obj, get: obj => obj.pwrite }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _create_decorators, { kind: "method", name: "create", static: false, private: false, access: { has: obj => "create" in obj, get: obj => obj.create }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _mkdir_decorators, { kind: "method", name: "mkdir", static: false, private: false, access: { has: obj => "mkdir" in obj, get: obj => obj.mkdir }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _unlink_decorators, { kind: "method", name: "unlink", static: false, private: false, access: { has: obj => "unlink" in obj, get: obj => obj.unlink }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _rmdir_decorators, { kind: "method", name: "rmdir", static: false, private: false, access: { has: obj => "rmdir" in obj, get: obj => obj.rmdir }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _rmR_decorators, { kind: "method", name: "rmR", static: false, private: false, access: { has: obj => "rmR" in obj, get: obj => obj.rmR }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _rename_decorators, { kind: "method", name: "rename", static: false, private: false, access: { has: obj => "rename" in obj, get: obj => obj.rename }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _copy_decorators, { kind: "method", name: "copy", static: false, private: false, access: { has: obj => "copy" in obj, get: obj => obj.copy }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _dirCopy_decorators, { kind: "method", name: "dirCopy", static: false, private: false, access: { has: obj => "dirCopy" in obj, get: obj => obj.dirCopy }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _truncate_decorators, { kind: "method", name: "truncate", static: false, private: false, access: { has: obj => "truncate" in obj, get: obj => obj.truncate }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _setattr_decorators, { kind: "method", name: "setattr", static: false, private: false, access: { has: obj => "setattr" in obj, get: obj => obj.setattr }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _search_decorators, { kind: "method", name: "search", static: false, private: false, access: { has: obj => "search" in obj, get: obj => obj.search }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _filesContaining_decorators, { kind: "method", name: "filesContaining", static: false, private: false, access: { has: obj => "filesContaining" in obj, get: obj => obj.filesContaining }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _linesContaining_decorators, { kind: "method", name: "linesContaining", static: false, private: false, access: { has: obj => "linesContaining" in obj, get: obj => obj.linesContaining }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _beforeFullWalk_decorators, { kind: "method", name: "beforeFullWalk", static: false, private: false, access: { has: obj => "beforeFullWalk" in obj, get: obj => obj.beforeFullWalk }, metadata: _metadata }, null, _instanceExtraInitializers); if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); } [VFS_BRAND] = (__runInitializers(this, _instanceExtraInitializers), true); name = 'base'; get conditionSupport() { return {}; } vfsMd = ''; indexTtl = 600; /** * Whether reads of this VFS may be served from / written to the * local file cache. A network-backed VFS whose content is read-mostly * (e.g. object storage) sets this to true so reads can be cached; a * VFS whose content is live (e.g. a database collection) leaves it * false so reads always hit the backend and live follows (`tail -f`) * are not masked by a cached snapshot. */ cachesReads = false; /** * Whether this VFS carries enough version information for * snapshot+replay drift detection. When true, the driver's `stat` op * must populate {@link FileStat.fingerprint} * (and optionally {@link FileStat.revision}) with stable per-path * markers. When false (the default), reads are treated as live-only * at replay time: no fingerprint is captured at snapshot, no drift * check fires at load. */ supportsSnapshot = false; /** * Whether the driver's `stat` op can size every regular file without * fetching its content, i.e. {@link FileStat.size} is null only for * directories. True for byte stores that keep a length in their * metadata (ram, disk, redis, s3, gridfs); false for mounts that * render content on read, where the size is unknowable until the bytes * exist (slack, gmail, notion, postgres rows.jsonl, dify documents). * * FUSE does not need this: direct_io + attrTimeout '0' + hydrate-on-open * make size-unknown files read correctly anyway. FSKit has no direct_io * equivalent, so a mount there is driven entirely by the reported size * and a false VFS would serve silent empty files. Mirrors Python's * `BaseVFS.sizes_always_known`. */ sizesAlwaysKnown = false; /** * Whether a `read: fresh` mount can actually be revalidated against this * backend: the driver's `stat` op and the read record must stamp the * *same kind* of content token, so the gate can compare them with `===`. False * (the default) is refused at mount time rather than degraded, because a * mount that declares fresh and silently serves bounded is the bug the * policy exists to prevent. * * Distinct from {@link BaseVFS.supportsSnapshot}, which asks whether a * token exists at all, and from {@link BaseVFS.cachesReads}, which asks * whether the gate can fire. A backend can have a token on both sides * and still fail this one, by stamping two different kinds. * * A declarer must stamp the token on every read, not only while a * recorder is active: node's read_revalidatable.test.ts holds each one to * that (#1165). onedrive and sharepoint qualify because every unpinned * byte read fetches the item's cTag before its bytes, recorded or not; a * stream stamps only under a recorder, the one place its token can land. * * Mirrors Python's `BaseVFS.read_revalidatable`. */ readRevalidatable = false; /** * What a `read: fresh` mount checks a cached listing against before it * lists again: nothing (NONE, the default), one version for the whole mount * answered by a stat of its root (MOUNT), or each folder's own version * answered by a stat of that folder (FOLDER). A declarer stores with each * listing it writes a version no newer than its rows: taken from the same * response (github's tree names its head), or read first and the rows then * read at it or after it (hf walks the tree at the commit its revision * request answered; disk stats a folder before it scans), so a change in * between leaves the stored version behind and the next check re-lists. * Its `stat` must answer the same kind of token: node's * listing_version.test.ts holds each one to that. * * Mirrors Python's `BaseVFS.listing_version`. */ listingVersion = ListingVersion.NONE; /** * The stats each of this VFS's functions is handed, by method name: a * function named here gets a `Handed` holding each stat it lists, found * in the mount's stored listings, where it takes its index otherwise. * Python reads the same off each function's parameters. */ handed = {}; /** * The version every listing of this mount is pinned at, when its ref names * a commit outright (github's full-sha ref; see `pinOf` in * vfs/github/github.ts for why that cannot move). A stored listing whose * version equals it is served without a check. It depends on the mount's config, so * an instance sets it; null pins nothing. */ listingsPin = null; /** * Whether `read` fetches a byte window from the store itself. When false * the caller reads the whole file and slices it, so `read` is only ever * handed a window by a VFS that sets this. */ readsRanges = false; /** * Whether the data lives on the host filesystem, which lets a command * aggregate on the host instead of streaming through mirage. */ local = false; /** The store's capacity and counted bytes, set by a backend whose config * caps it; null when uncapped. */ usage = null; /** * The files `filesContaining` answers for, as mount-relative globs matched * segment by segment (a glob naming a directory covers what is below it); * null for every file. grep and rg read any other file whatever the * answer, as a full scan would. */ searchable = null; /** * `filesContaining` answers only a whole-word search (grep -w, rg -w); * any other pattern reads every file. */ wholeWordSearch = false; /** * The most files one command walks under a directory of this mount: grep * -r, rg and git grep stop reading there, find, ls -R, tree, du and * getfattr -R stop listing, git status and the other verbs that walk a * work tree fail, a glob matching more is refused, and so are tar -c, zip * -r, cp -r, rm -r, chmod -R, chown -R, chgrp -R, mv to another mount and * diff -r of a directory holding more (`beforeFullWalk`); -1 for no cap. * This is the backend's own value: the mount's `'@all'` command limit, else * the workspace's, replaces it when the VFS is mounted. */ walkLimit = WALK_LIMIT; /** * Extensions whose `read` is a rendering rather than the stored bytes, * each to the name of the method that renders it, which takes `read`'s * arguments, window included. A rendered read is never served from or * kept in the file cache, and a `raw` read asks for `read` itself. */ renderers = {}; /** * The backend handle every function takes. A builtin declares and * assigns its own; one that brings none runs over a no-op accessor. */ accessor = NO_ACCESSOR; #commands; #closed = false; /** * Set the facts a subclass does not declare as members. Every option is * optional, so a class that declares its facts as members calls * `super()` bare. */ constructor(options = {}) { if (options.name !== undefined) { if (options.name === '') throw new Error('a VFS needs a non-empty name'); this.name = options.name; } if (options.accessor !== undefined) this.accessor = options.accessor; if (options.vfsMd !== undefined) this.vfsMd = options.vfsMd; if (options.cachesReads !== undefined) this.cachesReads = options.cachesReads; if (options.sizesAlwaysKnown !== undefined) this.sizesAlwaysKnown = options.sizesAlwaysKnown; if (options.supportsSnapshot !== undefined) this.supportsSnapshot = options.supportsSnapshot; if (options.readRevalidatable !== undefined) { this.readRevalidatable = options.readRevalidatable; } this.#commands = [...(options.commands ?? [])]; } /** * Whether this VFS defines the function `name`. A function the base * declares is supported once a subclass or the instance itself replaces * it; one only a subclass declares (a custom `@vfsCall`) is supported * because it exists. */ supports(name) { const method = methodName(name); const own = this[method]; return (typeof own === 'function' && own !== BaseVFS.prototype[method]); } /** The bespoke commands and command trees this VFS was handed. */ commands() { return this.#commands; } /** * List the children of a directory, each as its stat answers it. * * On a walk, `handed.limit` is the most entries the walk can still use; * a readdir may stop once it holds that many and answer them as * `new DirListing({ entries, partial: true })`. A glob hands the word it * matches as `path.pattern`; a backend that filters by it answers * `partial: true` too. Mirrors Python's `readdir`. */ readdir(path, _handed) { return Promise.reject(enotsup(this.name, 'readdir', path)); } /** * Read a file's bytes, or a window of them. Answer with the bytes, or with * an async iterable that hands them out as the caller pulls, so a large * file never sits in memory: an `async` method that resolves to either, or * an `async *` one that yields chunks. Mirage collects a stream for a * caller that needs the bytes whole and hands bytes to a streaming caller * as one chunk. A window reaches this only when `readsRanges` is set: the * caller otherwise reads the whole file and slices it. */ read(path, _handed, _offset = 0, _size = null, _signal) { return Promise.reject(enotsup(this.name, 'read', path)); } /** Describe a path; rejects with ENOENT when nothing is there. */ stat(path, _handed) { return Promise.reject(enotsup(this.name, 'stat', path)); } /** Whether anything is at `path`; derived from `stat` when not defined. */ exists(path, _handed) { return Promise.reject(enotsup(this.name, 'exists', path)); } /** * The first `n` lines of a file, without reading the rest. * * `head -n N` asks this before it reads, so a backend that can fetch the * start of a file on its own (a `LIMIT` on a table) does not send the whole * file. The answer stands in for the file: exactly the first `n` lines a * read gives, each with its newline, and the whole file when it has fewer. * A file the read cache holds is read instead. Resolves null to have the * file read the usual way; rejects with the file's read error otherwise * (`EFBIG` when the answer passes a ceiling on one read, such as * `max_read_rows`). */ firstLines(path, _n, _handed) { return Promise.reject(enotsup(this.name, 'firstLines', path)); } /** * The last `n` lines of a file, without reading the rest. * * `tail -n N` asks this before it reads, and `tail -f` for its first print. * The answer stands in for the file: exactly the last `n` lines a read * gives, each with its newline, and the whole file when it has fewer. A * file the read cache holds is read instead. Resolves null to have the file * read the usual way; rejects with the file's read error otherwise (`EFBIG` * when the answer passes a ceiling on one read). */ lastLines(path, _n, _handed) { return Promise.reject(enotsup(this.name, 'lastLines', path)); } /** * How many newlines a read of the file gives, without reading it. * * `wc -l` asks this before it reads (a `COUNT(*)` on a table), and only * when it counts lines alone. A file the read cache holds is read instead. * Resolves null to have the file read the usual way; rejects with the * file's read error otherwise. */ lineCount(path, _handed) { return Promise.reject(enotsup(this.name, 'lineCount', path)); } /** * The digest the backend keeps for a file, without reading it. * * `md5sum` and its siblings ask this before they read, except under `-c`, * which checks the bytes themselves, so a backend that stores a file's * digest (an object's MD5 ETag, a drive's `md5Checksum`) does not send the * file. Answer only a digest of exactly the bytes a read gives, in * lowercase hex: a stored value that can be anything else (a multipart or * encrypted object's ETag) is null. `algorithm` is the hashlib name, `md5`, * `sha1`, `sha224`, `sha256`, `sha384`, `sha512` or `blake2b`, or * `blake2b-N` for BLAKE2b at N bits. A file the read cache holds is read * instead. Resolves null to have the file read and hashed; rejects * with the file's read error otherwise. */ checksum(path, _algorithm, _handed) { return Promise.reject(enotsup(this.name, 'checksum', path)); } /** * The last `n` lines of a file, then each line added to it, as it lands. * * `tail -f` on one file asks this for all it prints, so a backend that can * be told about new data (a change stream) does not have to be polled, one * that can be asked for only what is new does not read the whole file again * on each poll, and the lines it prints first and the ones it follows meet * at one point: none is lost or printed twice. The stream yields whole * lines, each with its newline, in the order a read would show them, and * ends only when the caller closes it; closing it ends a wait for the next * line. `interval` is `tail -s`, the seconds between two asks for what is * new; a change stream has no use for it. Resolves null to have `tail` read * the file and poll it every `-s` seconds; rejects with the file's read * error otherwise. */ follow(path, _n, _interval, _handed) { return Promise.reject(enotsup(this.name, 'follow', path)); } /** * Every entry under a directory, in one answer. * * `find`, `du`, `ls -R`, `tree`, `grep -r` and `rg` ask this before they * walk a directory, so a backend that can list a subtree on its own (a * recursive listing, one query over many folders) does not take a * `readdir` per directory. The answer stands in for the walk: every file and * directory below `path`, keyed by its path below `path` (`a`, `a/b.txt`), * each with the stat `stat` gives for it. The walk lists each directory from it in name order. * Resolve null to have the command walk `readdir` the usual way; reject with * what `readdir` of `path` rejects with otherwise. `handed.limit` is the * most entries the walk can still use; resolve null once the tree holds * more. A walk that reads one directory at a time asks with `level: true`, * which the mount answers from its own listing (the directory's children, * each with its stat) and never asks this function. Mirrors Python's * `read_tree`. */ readTree(path, _handed) { return Promise.reject(enotsup(this.name, 'readTree', path)); } /** * Replace a file's bytes, creating it when missing. `data` is the whole * content, or an async iterable that hands it over as it is produced (a * program's output redirected into the file); `materialize(data)` collects * it for a store that takes one body. A stream that lands as it arrives * lets a reader watch the file grow, and `atomic` asks that nothing show * until it has ended whole, which a write that collects first already does. */ write(path, _data, _signal, _handed, _atomic = false) { return Promise.reject(enotsup(this.name, 'write', path)); } async _replace(request, _signal, _handed) { await closeQuietly(request.body); throw enotsup(this.name, 'conditional replacement', request.path); } /** * Add bytes to the end of a file, creating it when missing. `data` is * bytes or a stream, as `write` takes it. A VFS that defines `write` and * not this is appended to by reading the file and writing it back. */ append(path, _data, _handed, _signal) { return Promise.reject(enotsup(this.name, 'append', path)); } /** * Write bytes at an offset, keeping every byte outside them. As * pwrite(2): a gap past the end reads back as zeros and a missing file * is created. A VFS that defines `write` and not this is written by * reading the file and writing it back. */ pwrite(path, _data, _offset, _handed, _signal) { return Promise.reject(enotsup(this.name, 'pwrite', path)); } /** Create an empty file, leaving an existing one as it is. */ create(path, _handed) { return Promise.reject(enotsup(this.name, 'create', path)); } /** Make a directory; `parents` makes missing parents too, as `mkdir -p`. */ mkdir(path, _parents = false, _handed) { return Promise.reject(enotsup(this.name, 'mkdir', path)); } /** Remove a file. */ unlink(path, _handed) { return Promise.reject(enotsup(this.name, 'unlink', path)); } /** * Remove an empty directory. The index is the mount's, which a refused * rmdir's hidden-remnant walk lists through. A store that checks * emptiness, then deletes, looks again after: a child written in between * keeps the directory, which stays as it was, and the call rejects with * ENOTEMPTY. */ rmdir(path, _handed) { return Promise.reject(enotsup(this.name, 'rmdir', path)); } /** * Remove a subtree in one call instead of entry by entry. A store that * lists, then deletes, looks again after: a child written under the root * since its listing stays, inside its directories, and the call rejects * with ENOTEMPTY on the root, never success over a key it left. */ rmR(path, _handed) { return Promise.reject(enotsup(this.name, 'rmR', path)); } /** * Move a name within this VFS. A store that moves a directory by listing * it moves only what it listed: a child written under `src` since stays * there, inside its directories, and the call rejects with ENOTEMPTY on * `src`. */ rename(src, _dst, _handed) { return Promise.reject(enotsup(this.name, 'rename', src)); } /** Copy a file within this VFS without moving its bytes through mirage. */ copy(_src, dst, _signal, _handed) { return Promise.reject(enotsup(this.name, 'copy', dst)); } /** Copy a directory tree within this VFS in one call. */ /** * Copy a file from another mount onto this one without moving its bytes * through mirage. * * `cp` and `mv` between two mounts ask this before they read the file and * write it here, once the dispatcher has judged the source as a read and * the destination as a create. It is no dispatcher call of its own, so * nothing reaches it unjudged. `source` is the source mount's VFS and `src` * the file there. Reject EXDEV when this backend cannot copy from `source` * (another kind of store, other credentials, a refusal), and the bytes move * through the read and the write instead. */ copyFrom(_source, _src, dst) { return Promise.reject(enotsup(this.name, 'copyFrom', dst)); } dirCopy(_src, dst, _handed) { return Promise.reject(enotsup(this.name, 'dirCopy', dst)); } /** * Resize a file, padding with zeros or cutting the end. `noCreate` * refuses a missing file instead of creating it; a VFS that cannot hold * that atomically rejects with ENOTSUP before writing. */ truncate(path, _length, _noCreate = false, _handed) { return Promise.reject(enotsup(this.name, 'truncate', path)); } /** * Store metadata fields the backend keeps itself. Resolves to the fields * it stored; the rest land in the namespace's attribute overlay. */ setattr(path, _fields, _handed) { return Promise.reject(enotsup(this.name, 'setattr', path)); } /** * Search the resource under `path`; null declines, [] is none. One * `[path, record]` pair per hit: the hit's virtual path, which the * dispatcher drops the hit for when the session hides it, and the record * the `search` CLI prints, a header line naming the path (`path:score` * where the store ranks) and then the hit's text. Errors and incomplete * results reject, never answered as a miss. */ search(path, _query, _handed) { return Promise.reject(enotsup(this.name, 'search', path)); } /** Search several scopes as one ranked query. */ searchMany(paths, _query, _handed) { return Promise.reject(enotsup(this.name, 'search', paths[0] ?? '')); } /** * Read several whole files in one request, for a read-ahead. Keyed by each * answered path's `virtual`. A file may be left out (one larger than * `limit` bytes, one the request could not read), and its own read answers * it later; an error of the whole request rejects. Each file read is * recorded as `read` records it, fingerprint included, so the read cache * keeps its token. A VFS that needs what the listings hold declares * `readMany` in `handed` and gets each path's `Handed`, in the order of * `paths`. Mirrors Python's `read_many`. */ readMany(paths, _limit, _handed) { return Promise.reject(enotsup(this.name, 'read', paths[0] ?? '')); } /** * Files under `path` whose content may contain `text`. grep and rg still * walk, filter, order and label every file, and read only the ones answered * here, matched on `vfsPath` without case, so an extra file costs a read and * a missing one is a wrong answer. A search that holds only keys names each one with * `mountedPath(path, '/' + key)`. Resolve null when the answer may be * incomplete (an error, a truncated result, an index that lags writes), and * every file is read; reject to refuse the command, and the error's message * is what it prints. The dispatcher drops every file the session hides * from the answer. * * @param path a directory walked: one of grep's directory operands under -r * or -R, or of rg's operands or the cwd; asked once per directory. * @param text plain text every match holds, never a pattern: the pattern * or each -e of grep and rg, or for a regex a fixed piece of at least * three characters every match contains. Asked once per text; a file any * answer holds is read. * @param opts `wholeWord` is true under -w or -x with a plain-text * pattern, where `text` is a whole word of every match, and false * otherwise, where it may sit inside a word. `ignoreCase` is true under * -i, and under rg -S with a lowercase pattern; folding case when false * is fine. */ filesContaining(_path, _text, _opts, _handed) { return Promise.resolve(null); } /** * The lines of `path` that may contain `text`, in file order. grep and rg * match each line themselves, so an extra line is fine and a missing one is * a wrong answer. Each line keeps the newline the file has, whole or * streamed. The lines stand in for the file when the output shows no line * positions (no -n, -b, --column, --vimgrep), no context (no -A, -B, -C) and * there is one text; otherwise the file is read only when some answer holds * a line, and only a stream's first chunk is pulled. null reads the file; * answer null for a file that may hold a NUL byte, since grep and rg call * such a file binary from bytes outside its matching lines. Reject to refuse * the command. * * @param path the file. * @param text plain text every match holds, as `filesContaining` gets it. * @param opts `ignoreCase` is true under -i, and under rg -S with a * lowercase pattern. */ linesContaining(_path, _text, _opts, _handed) { return Promise.resolve(null); } /** * Called before a command acts on every file under `path`, once per * directory a command copies, compares, changes or removes whole, before * the first file it touches there: tar -c, zip -r, cp -r, mv across * mounts, diff -r, rm -r, chmod -R, chown -R and chgrp -R. Resolve to let * the walk run; reject to refuse it, and the error's message is what the * command prints. The dispatcher has already refused more than the * command's `walkLimit` files, so the default lets the walk run. Searches * and listings are never refused: they stop after `walkLimit` files. * * @param path the directory about to be walked. * @param files how many files the session sees under `path`, with every * hidden file dropped: the mount's `readTree`, or under a * `walkLimit` a walk of its listings that stops one file past it; * null when neither counts, and the guard counts them itself. */ beforeFullWalk(_path, _files, _handed) { return Promise.resolve(); } /** Whether the backend is there to answer at all. */ isMounted() { return true; } /** * Where this driver's bytes live, as one string a person can read: * `disk:/srv/data`, `s3:aws:my-bucket/prefix`. Two mounts with the * same location address the same bytes, which is how `cp` and `mv` * across mounts refuse to copy a file onto itself. Null, the default, * means unknown, and the mount then treats this instance as a location * of its own, which is the safe direction to be wrong in: a false * "different" only keeps the pre-existing behavior, while a false * "same" would refuse a legitimate move. A driver whose config pins * the storage (a disk root, a bucket and key prefix) overrides this so * two instances pointing at one target compare equal. */ storageLocation() { return null; } /** * How much space this backend has, for `df`. The default is UNKNOWN, * which `df` renders as `-`. A driver that can answer truthfully (a * real filesystem, a provider that exposes a storage quota) overrides * this. Never fabricate a number. */ capacity() { return Promise.resolve({ state: CapacityState.UNKNOWN }); } /** * What a snapshot records for this driver: its type and `needs_override`, * since the base cannot know a subclass's constructor, so both loaders * then require the mount to be handed back live (`load`'s overrides; * `copy()` does this itself). A driver that owns its content (an * in-memory store) overrides this and {@link BaseVFS.loadState} to carry * it and drops the flag; a driver over a remote service keeps the default * and pins what it read through `supportsSnapshot` fingerprints instead. * `toStateDict` calls this and `loadState` on every mount, which is why * neither is optional. Mirrors Python `BaseVFS.get_state`. */ getState() { return { type: this.name, needs_override: true }; } /** Snapshot a config-backed driver through its schema's secret markers. */ configState(schema, config) { return { type: this.name, config: redactConfigWithSchema(schema, config) }; } /** * Take back what {@link BaseVFS.getState} put out. A no-op by * default, because the bare `{type}` carries nothing to restore. * Mirrors Python `BaseVFS.load_state`. */ loadState(_state) { // Nothing to take back. } get isClosed() { return this.#closed; } /** * Release what this driver owns, exactly once: its accessor's handles * are its own to close, since the Accessor seam carries no lifecycle. * A backend with handles of its own (a db pool, an ssh channel) * overrides this and calls `super.close()`. Mirrors Python * `BaseVFS.close`. */ close() { this.#closed = true; return Promise.resolve(); } }; })(); export { BaseVFS }; //# sourceMappingURL=base.js.map