@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
163 lines • 6.29 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 { enoent } from "../../errors/fs.js";
import { Descriptor } from "../files.js";
import { entryName } from "../../utils/remnants.js";
import { filetypeOf } from "./stat.js";
/**
* The filesystem a wasm guest sees: the workspace mounts, through the
* runtimes' file API. Mirrors Python's `WasmView` for a guest that
* carries no files of its own (qjs), which is the only one this host
* runs: no interpreter build is routed beside the mounts.
*
* Its job is shape. `RuntimeFiles` answers in virtual paths and one
* `VFSStat` per path; preview1 asks in `[name, filetype]` pairs and
* errno, and its filestat record has no mode field at all, so the type
* bits are read out of the mode and the rest is dropped.
*
* @param files - the shared mount op vocabulary, its calls stopped with
* the run (`RuntimeFiles.withSignal`); null when no workspace is
* attached, so the guest sees nothing.
*/
export class WasmView {
files;
constructor(files = null) {
this.files = files;
}
requireFiles() {
if (this.files === null)
throw enoent('/');
return this.files;
}
/**
* The file adapter for a content call on `path`, inside the view only.
* Structure is open (a stat, a listing or a readlink answers for any
* path the workspace has), but content goes only through the runtime's
* view (`RuntimeFiles.serves`), the rule every guest file adapter keeps.
*/
filesFor(path) {
const files = this.files;
if (files?.serves(path) !== true)
throw enoent(path);
return files;
}
/** Stat a guest path. */
stat(path) {
return this.coreStat(path);
}
/**
* Stat a guest path without following a trailing symlink: a link's own
* row is the node table's, which the dispatcher serves under `nofollow`.
*/
lstat(path) {
return this.coreStat(path, true);
}
/**
* The adapter's own stat, or ENOENT when no workspace is attached. A
* path outside the view still answers as structure: a directory row
* when the workspace lists it (`RuntimeFiles.viewStat`).
*/
async coreStat(path, nofollow = false) {
const files = this.requireFiles();
if (files.serves(path))
return files.stat(path, nofollow);
const row = await files.viewStat(path);
if (row === null)
throw enoent(path);
return row;
}
/** The path's row, or null when the workspace does not have it. */
async statOrNull(path, nofollow = false) {
try {
return await (nofollow ? this.lstat(path) : this.stat(path));
}
catch (err) {
if (err.code === 'ENOENT')
return null;
throw err;
}
}
/**
* The mount's unclassified rows for `path`, or null. Asked of a path
* with no row; a path outside the view never lists here, its stat
* having already answered the directory row `viewStat` finds.
*/
async listingOrNull(path) {
const files = this.requireFiles();
return files.serves(path) ? files.listingOrNull(path) : null;
}
/** A file's bytes, or a range of them. */
read(path, offset = 0, size, raw = false) {
return this.filesFor(path).read(path, { offset, ...(size !== undefined ? { size } : {}), raw });
}
/** Open a workspace file on the run's core, the open's effect landing there. */
async open(path, mode) {
const files = this.filesFor(path);
return new Descriptor(files, await files.open(path, mode), mode, path, false, files.walked(path));
}
unlink(path) {
return this.filesFor(path).unlink(path);
}
mkdir(path) {
return this.filesFor(path).mkdir(path);
}
rmdir(path) {
return this.filesFor(path).rmdir(path);
}
/** Rename within the workspace. */
rename(src, dst) {
this.filesFor(dst);
return this.filesFor(src).rename(src, dst);
}
/** Create a symlink at `path` pointing at `target`, stored as typed. */
symlink(path, target) {
return this.filesFor(path).symlink(path, target);
}
/** Where the namespace's links lead `path` (`RuntimeFiles.walked`). */
walked(path, follow = true) {
return this.files === null ? path : this.files.walked(path, follow);
}
/** The target of the symlink at `path`; EINVAL when it is no link. */
readlink(path) {
if (this.files === null)
throw enoent(path);
return this.files.readlink(path);
}
/**
* Write timestamps, in the namespace overlay where needed. Times are
* the only attributes preview1 can express.
*/
setattr(path, atime, mtime, nofollow) {
return this.filesFor(path).setattr(path, {
...(atime !== null ? { atime } : {}),
...(mtime !== null ? { mtime } : {}),
nofollow,
});
}
/**
* List a guest directory as `[name, preview1 filetype]` pairs. Core
* entries arrive kind-resolved, and a link is reported as one, so a
* guest that reads `d_type` answers `is_symlink` without a call.
*/
async readdir(path) {
const entries = new Map();
for (const entry of await this.requireFiles().readdir(path)) {
const base = entryName(entry.path);
if (base !== '')
entries.set(base, filetypeOf(entry));
}
return [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
}
}
//# sourceMappingURL=view.js.map