@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
752 lines • 28.1 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 { dottedSpelling, resolvePath } from "./utils/path.js";
import { rstripSlash, stripSlash } from "./utils/slash.js";
/**
* What a mount lets commands do, a cumulative ladder. READ lists and
* reads. WRITE also creates, changes and deletes. EXEC also lets the
* interpreter commands (python3, js) run code: a script file named as
* their operand must sit on an EXEC mount, while inline code and -m need
* one somewhere in the workspace (imports are not checked by path).
* Shell scripts need no EXEC, since the shell checks each of their
* commands like a typed one; a ./script whose shebang names python3
* reaches python3, which does check.
*/
export const MountMode = Object.freeze({
READ: 'read',
WRITE: 'write',
EXEC: 'exec',
});
/**
* How a mount is exposed to the outside world.
*
* `workspace` is the default: the mount lives only inside mirage's own filesystem
* and is reached through the command surface, with nothing registered with
* the kernel. `fuse` and `fskit` additionally expose it as a real mountpoint.
*
* `fskit` is macOS 15.4+ only and needs no kernel extension. It has no
* `direct_io` equivalent, so it serves correct reads only for mounts that
* set `sizesAlwaysKnown`; the mount-time guard warns about mounts whose
* size-unknown files will read as empty. Writes are also limited: appends and
* metadata ops persist, but the macFUSE FSKit shim flushes pages a file did
* not already have (a new file, or truncate-then-write) as NUL bytes (pinned
* in `integ/fuse/truth_fskit.json`). There is deliberately no `auto`:
* auto-selecting fskit would silently degrade every API-backed mount.
*
* `nfs` mounts through the operating system's own NFS client, so nothing is
* installed: mirage serves NFSv3 on a local port and runs the system `mount`.
* Linux and macOS; Linux needs root to mount. It is read-only for now, and
* like fskit it reads only as far as the size a file reports, so it serves
* mounts that set `sizesAlwaysKnown`.
*/
export const MountBackend = Object.freeze({
WORKSPACE: 'workspace',
FUSE: 'fuse',
FSKIT: 'fskit',
NFS: 'nfs',
});
/** Backends that register a real mountpoint with the kernel. */
export const KERNEL_BACKENDS = Object.freeze([
MountBackend.FUSE,
MountBackend.FSKIT,
MountBackend.NFS,
]);
export const MOUNT_MODE_RANK = Object.freeze({
[MountMode.READ]: 1,
[MountMode.WRITE]: 2,
[MountMode.EXEC]: 3,
});
/** The weaker of two mount modes on the READ < WRITE < EXEC lattice. */
export function weakerMode(a, b) {
return MOUNT_MODE_RANK[a] <= MOUNT_MODE_RANK[b] ? a : b;
}
export const DEFAULT_VISIBILITY = Object.freeze({
paths: null,
shown: null,
vars: null,
processes: 'session',
});
const MOUNT_MODE_ALIASES = Object.freeze({
r: MountMode.READ,
rw: MountMode.WRITE,
rwx: MountMode.EXEC,
});
/**
* Coerce a mount mode, accepting cumulative filesystem aliases.
*
* The mode ladder is cumulative (exec implies write implies read), so
* only the cumulative spellings `r`, `rw`, `rwx` alias the modes;
* bit-style forms like `w` or `x` are rejected.
*/
export function parseMountMode(value) {
const alias = MOUNT_MODE_ALIASES[value];
if (alias !== undefined)
return alias;
if (Object.values(MountMode).includes(value))
return value;
throw new Error(`invalid mount mode: '${value}'`);
}
/**
* How a mount decides whether cached bytes may be served.
*
* FRESH revalidates against the backend's content token before serving a
* cached copy; BOUNDED serves without revalidating, within the staleness
* bound the mount declares.
*
* PINNED names the content a commit's fingerprint records. There is no
* version layer to pin to, so a mount declaring it is refused at mount time
* rather than quietly degraded to head: the vocabulary is published, so
* someone will type it, and an informative refusal costs one branch over a
* generic invalid-value error.
*/
export const ReadPolicy = Object.freeze({
FRESH: 'fresh',
BOUNDED: 'bounded',
PINNED: 'pinned',
});
/**
* Whether replacements carry the version they were based on. Unconditional
* existing-file rewrites are last-write-wins. Shared rewrites that observed
* a missing file require atomic create-only under either policy.
* Conditional writes also protect the version the mount last saw. Staged
* names a staging layer not implemented here and is refused at mount time.
* Mirrors Python's `WritePolicy`.
*/
export const WritePolicy = Object.freeze({
UNCONDITIONAL: 'unconditional',
CONDITIONAL: 'conditional',
STAGED: 'staged',
});
/**
* What a backend's cached listings can be checked against under fresh.
*
* NONE: nothing, so a listing the running command did not write itself is
* listed again. MOUNT: one version covers every listing of the mount, and a
* stat of the mount root answers it. FOLDER: each listing carries its own
* folder's version, and a stat of that folder answers it. The stored version
* and the stat's fingerprint must be the same kind of token, since the gate
* compares them with `===`.
*/
export const ListingVersion = Object.freeze({
NONE: 'none',
MOUNT: 'mount',
FOLDER: 'folder',
});
/** Maximum lifetime in seconds for cached bodies and listings. */
export const DEFAULT_READ_TTL = 600;
/**
* The policy a mount takes when it declares none, and the one pinned onto the
* three synthetic anchors (`/dev`, the history view, the implicit `/` root).
*
* Those three are installed outside `normalizeMounts`, so they never meet the
* capability verdict; none of them caches reads, so inheriting a workspace-level
* `fresh` would stamp on them exactly the combination the verdict refuses.
*/
export const DEFAULT_READ_SPEC = Object.freeze({
policy: ReadPolicy.BOUNDED,
ttl: DEFAULT_READ_TTL,
});
/**
* Behaviour when a remote VFS's live fingerprint differs from the
* value recorded at snapshot time.
*/
export const DriftPolicy = Object.freeze({
/** Raise ContentDriftError on first mismatch. */
STRICT: 'strict',
/** Skip drift checks entirely. */
OFF: 'off',
});
/**
* Behaviour when a command's output exceeds its limit cap.
* TRUNCATE returns the truncated bytes + a notice on stderr.
* ERROR returns no stdout and exits 1 with the same notice.
*/
export const OnExceed = Object.freeze({
ERROR: 'error',
TRUNCATE: 'truncate',
});
function preferError(values) {
return values.some((v) => v === OnExceed.ERROR) ? OnExceed.ERROR : OnExceed.TRUNCATE;
}
function minBound(values) {
const bounds = values.filter((v) => v !== null);
return bounds.length > 0 ? Math.min(...bounds) : null;
}
function minPositive(values) {
const positives = values.filter((v) => v !== null && v > 0);
return positives.length > 0 ? Math.min(...positives) : null;
}
function tightestWalk(values) {
const present = values.filter((v) => v !== null);
const caps = present.filter((v) => v >= 0);
if (caps.length > 0)
return Math.min(...caps);
return present.length > 0 ? -1 : null;
}
/**
* A bound on a result: the policy layer's limit arm and the shape
* every cap config parses into. Carries its fields inline (the Deny
* precedent: an action is its payload). `kind` is the wire
* discriminant; `aggr` is the composition law (AND to the tightest
* per bound, ANY on error mode) and `layered` the resolution law (each
* field from the most specific layer that sets it). `walkLimit` caps the
* entries one walk lists on a mount, -1 for no cap.
*/
export class Limit {
kind = 'limit';
maxBytes;
maxLines;
timeoutSeconds;
walkLimit;
/** The fields the limit names, null among them; the rest inherit. Mirrors Python's model_fields_set. */
fieldsSet;
exceed;
constructor(init = {}) {
const maxBytes = init.maxBytes ?? null;
const maxLines = init.maxLines ?? null;
const timeoutSeconds = init.timeoutSeconds ?? null;
const walkLimit = init.walkLimit ?? null;
if (maxBytes !== null && (!Number.isInteger(maxBytes) || maxBytes < 0)) {
throw new TypeError(`maxBytes must be a non-negative integer, got ${String(maxBytes)}`);
}
if (maxLines !== null && (!Number.isInteger(maxLines) || maxLines < 0)) {
throw new TypeError(`maxLines must be a non-negative integer, got ${String(maxLines)}`);
}
if (timeoutSeconds !== null && (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0)) {
throw new TypeError(`timeoutSeconds must be a non-negative number, got ${String(timeoutSeconds)}`);
}
if (walkLimit !== null && (!Number.isInteger(walkLimit) || walkLimit < -1)) {
throw new TypeError(`walkLimit must be an integer of at least -1, got ${String(walkLimit)}`);
}
this.maxBytes = maxBytes;
this.maxLines = maxLines;
this.timeoutSeconds = timeoutSeconds;
this.walkLimit = walkLimit;
this.exceed = init.onExceed ?? null;
this.fieldsSet = new Set(Object.keys(init).filter((key) => init[key] !== undefined));
}
get onExceed() {
return this.exceed ?? OnExceed.TRUNCATE;
}
/**
* Each field from the first limit that names it, most specific first: a
* layer naming one bound inherits the others from the layers below it, and
* one naming a bound as null lifts it. Returns null when nothing is
* configured. Mirrors Python's Limit.layered.
*/
static layered(limits) {
const present = [...limits].filter((s) => s !== null);
if (present.length === 0)
return null;
const init = {};
for (const key of LIMIT_FIELDS) {
const layer = present.find((s) => s.fieldsSet.has(key));
if (layer !== undefined)
Object.assign(init, { [key]: layer.named(key) });
}
return new Limit(init);
}
named(key) {
return key === 'onExceed' ? this.onExceed : this[key];
}
/**
* Aggregate several limits using each field's declared rule.
*
* Reads the rules off LIMIT_AGGR rather than naming the fields here, so a
* new bound composes as soon as it is declared — the same property Python
* gets from walking `model_fields` for each field's `Aggr(rule)`. Returns
* null when nothing is configured. Used wherever bounds stack (policy
* composition, cross-mount fan-out, layered configs).
*/
static aggr(limits) {
const present = [...limits].filter((s) => s !== null);
if (present.length === 0)
return null;
const init = {};
for (const key of Object.keys(LIMIT_AGGR)) {
if (present.some((s) => s.fieldsSet.has(key)))
Object.assign(init, { [key]: LIMIT_AGGR[key](present) });
}
return new Limit(init);
}
}
const LIMIT_FIELDS = [
'maxBytes',
'maxLines',
'timeoutSeconds',
'walkLimit',
'onExceed',
];
/**
* Each bound's composition law, keyed by field.
*
* The mapped type is what makes this a table rather than a second copy of the
* field list: declaring a new bound on Limit without adding its rule here is a
* compile error, where the old inline literal would have silently dropped it.
*/
const LIMIT_AGGR = {
maxBytes: (present) => minBound(present.map((s) => s.maxBytes)),
maxLines: (present) => minBound(present.map((s) => s.maxLines)),
timeoutSeconds: (present) => minPositive(present.map((s) => s.timeoutSeconds)),
walkLimit: (present) => tightestWalk(present.map((s) => s.walkLimit)),
onExceed: (present) => preferError(present.map((s) => s.onExceed)),
};
export const VFSName = Object.freeze({
DISK: 'disk',
S3: 's3',
RAM: 'ram',
GITHUB: 'github',
WANDB: 'wandb',
LINEAR: 'linear',
GCAL: 'gcal',
GDOCS: 'gdocs',
GSHEETS: 'gsheets',
GSLIDES: 'gslides',
GDRIVE: 'gdrive',
ONEDRIVE: 'onedrive',
SHAREPOINT: 'sharepoint',
DROPBOX: 'dropbox',
BOX: 'box',
AIRTABLE: 'airtable',
SLACK: 'slack',
DISCORD: 'discord',
GMAIL: 'gmail',
TRELLO: 'trello',
MONGODB: 'mongodb',
GRIDFS: 'gridfs',
NOTION: 'notion',
LANGFUSE: 'langfuse',
JAEGER: 'jaeger',
SSH: 'ssh',
REDIS: 'redis',
GCS: 'gcs',
OCI: 'oci',
R2: 'r2',
EMAIL: 'email',
OPFS: 'opfs',
SUPABASE: 'supabase',
POSTGRES: 'postgres',
LANCEDB: 'lancedb',
CHROMA: 'chroma',
DIFY: 'dify',
MEM0: 'mem0',
QDRANT: 'qdrant',
HF_BUCKETS: 'hf_buckets',
HF_DATASETS: 'hf_datasets',
HF_MODELS: 'hf_models',
HF_SPACES: 'hf_spaces',
NEXTCLOUD: 'nextcloud',
DATABRICKS_VOLUME: 'databricks_volume',
TIDB_FILESYSTEM: 'tidb_filesystem',
MINIO: 'minio',
CEPH: 'ceph',
SEAWEEDFS: 'seaweedfs',
WASABI: 'wasabi',
BACKBLAZE: 'backblaze',
DIGITALOCEAN: 'digitalocean',
TENCENT: 'tencent',
ALIYUN: 'aliyun',
SCALEWAY: 'scaleway',
QINGSTOR: 'qingstor',
HISTORY: 'history',
BIN: 'bin',
});
/**
* How a command resolves a command-line operand's last component when it is
* a symlink: never (lstat(2): `rm`, `stat`), always (open(2): `cat`,
* `stat -L`), or only once a stat shows it leads to a directory (`ls` with no
* option that settles it).
*/
export const Follow = Object.freeze({
NONE: 'none',
LAST: 'last',
DIRECTORY: 'directory',
});
/**
* What one command line does, as its own command reads its options: the
* admission gate, the router and the fan-out ask this instead of reading the
* options themselves. `follows` is how its operands' last component
* resolves; `walks` that its traversal enters the mounts below its operands
* (`find`, `du`, `grep -r`; not `du -x`); `readsSubtree` that it reads below
* the paths it names, whether or not it crosses a mount (`du -x`, `tar -c`,
* `cp -r`); `writes` that it may change files, false only when its options
* leave a writing command nothing to change (`sort` without `-o`, `tar -t`);
* `subtree` that an operand is a whole subtree it moves or removes (`rm`,
* `rmdir`, `mv`); `moveTarget` the directory a move names by option
* (`mv -t DIR`), as typed, null when it names none. Mirrors Python's
* `LineFacts`.
*/
export class LineFacts {
follows;
walks;
readsSubtree;
writes;
subtree;
moveTarget;
constructor(init = {}) {
this.follows = init.follows ?? Follow.LAST;
this.walks = init.walks ?? false;
this.readsSubtree = init.readsSubtree ?? false;
this.writes = init.writes ?? true;
this.subtree = init.subtree ?? false;
this.moveTarget = init.moveTarget ?? null;
Object.freeze(this);
}
/** A copy with some fields replaced, the mirror of Python's `dataclasses.replace`. */
with(update) {
return new LineFacts({
follows: this.follows,
walks: this.walks,
readsSubtree: this.readsSubtree,
writes: this.writes,
subtree: this.subtree,
moveTarget: this.moveTarget,
...update,
});
}
}
export const FileType = Object.freeze({
DIRECTORY: 'directory',
FILE: 'file',
SYMLINK: 'symlink',
CHAR_DEVICE: 'char_device',
BLOCK_DEVICE: 'block_device',
FIFO: 'fifo',
SOCKET: 'socket',
});
/**
* A regular file's content shape: the rendering hint (file/ls color).
*
* Only meaningful for a FILE; a directory or symlink carries none. Not a
* node kind: nothing branches control flow on it. Mirrors `ContentType`
* in `mirage/types.py`.
*/
export const ContentType = Object.freeze({
TEXT: 'text',
BINARY: 'binary',
JSON: 'json',
CSV: 'csv',
IMAGE_PNG: 'image/png',
IMAGE_JPEG: 'image/jpeg',
IMAGE_GIF: 'image/gif',
ZIP: 'application/zip',
GZIP: 'application/gzip',
PDF: 'application/pdf',
});
// FileStat.extra key holding a symlink's target, verbatim as it was
// typed. A link has no backend inode, so this is the only place the
// target travels with the stat row.
export const LINK_TARGET_KEY = 'link_target';
// FileStat.extra key holding a synthetic device's logical [major, minor].
export const DEVICE_NUMBERS_KEY = 'device_numbers';
// FileStat.extra key marking a file whose bytes change on their own (a
// generated document): a kernel mount reads it fresh on every read rather
// than holding what an earlier read returned.
export const LIVE_KEY = 'mirage.live';
export class FileStat {
name;
size;
modified;
fingerprint;
revision;
type;
content;
mode;
uid;
gid;
atime;
ctime;
birthtime;
dev;
extra;
constructor(init) {
this.name = init.name;
this.size = init.size ?? null;
this.modified = init.modified ?? null;
this.fingerprint = init.fingerprint ?? null;
this.revision = init.revision ?? null;
this.type = init.type;
// content is a FILE's rendering hint; a directory or symlink has
// none. null on a FILE means "unknown", which is allowed.
const content = init.content ?? null;
if (content !== null && init.type !== FileType.FILE) {
throw new Error(`content must be null for ${init.type}, got ${content}`);
}
this.content = content;
this.mode = init.mode ?? null;
this.uid = init.uid ?? null;
this.gid = init.gid ?? null;
this.atime = init.atime ?? null;
this.ctime = init.ctime ?? null;
this.birthtime = init.birthtime ?? null;
this.dev = init.dev ?? null;
this.extra = init.extra ?? {};
Object.freeze(this);
}
// Copy with some fields replaced, the mirror of Python's
// `model_copy(update=...)`. Callers that rename a row (ls printing an
// operand as typed, a link row taking the link's name) must use this
// rather than re-listing fields at the call site: a hand-written list
// silently drops whatever it forgot, which is how link ownership went
// missing. This is the one place the field list is repeated, and it
// sits next to the declarations so an added field is hard to miss.
with(update) {
return new FileStat({
name: this.name,
size: this.size,
modified: this.modified,
fingerprint: this.fingerprint,
revision: this.revision,
type: this.type,
content: this.content,
mode: this.mode,
uid: this.uid,
gid: this.gid,
atime: this.atime,
ctime: this.ctime,
birthtime: this.birthtime,
dev: this.dev,
extra: this.extra,
...update,
});
}
}
/**
* A directory's listing as a backend answers it, when one fetch proves more
* than the directory's own children.
*
* A backend's `readdir` answers a plain list of its children's stats, or this
* when the fetch says more: a day of messages that also lists its `files/`
* subdirectory, a board whose lists come with their cards. The dispatcher
* keeps every listing it is handed, so entering a seeded directory costs no
* second identical fetch.
*
* - `entries`: the listed directory's own children, each filled in as `stat`
* would fill it.
* - `seeds`: descendant listings the same fetch proved, keyed by path relative
* to the listed directory (`files`, `2026-01-05/Report__17`).
* - `partial`: the entries are a filtered or truncated view (a glob the
* backend narrowed, a fetch it did not finish), so they are kept as entries
* but not as the directory's listing. `seeds` stay full listings of the
* children it did report.
* - `window`: the fetch was capped (the newest N messages, the last N days),
* so the entries are everything to show but prove nothing absent: a child
* that drops out of the window has not been deleted. Kept as listings that
* evict nothing.
* - `version`: the version the listing was read at, for a backend that
* declares a `listingVersion`; a `fresh` mount checks it before serving the
* listing again.
*/
export class DirListing {
entries;
seeds;
partial;
window;
version;
constructor(init) {
this.entries = init.entries;
this.seeds = init.seeds ?? new Map();
this.partial = init.partial ?? false;
this.window = init.window ?? false;
this.version = init.version ?? null;
Object.freeze(this);
}
}
export const FileChangeKind = Object.freeze({
CREATE: 'create',
UPDATE: 'update',
DELETE: 'delete',
MOVE: 'move',
UNKNOWN: 'unknown',
});
export class FileMetadata {
fingerprint;
size;
modified;
constructor(init = {}) {
this.fingerprint = init.fingerprint ?? null;
this.size = init.size ?? null;
this.modified = init.modified ?? null;
Object.freeze(this);
}
}
export class FileEvent {
kind;
path;
timestamp;
previousPath;
metadata;
constructor(init) {
this.kind = init.kind;
this.path = init.path;
this.timestamp = init.timestamp;
this.previousPath = init.previousPath ?? null;
this.metadata = init.metadata ?? null;
Object.freeze(this);
}
}
export class Delta {
changes;
checkpoint;
constructor(init) {
this.changes = Object.freeze([...init.changes]);
this.checkpoint = init.checkpoint;
Object.freeze(this);
}
}
export const OverflowPolicy = Object.freeze({
COLLAPSE: 'collapse',
DROP_OLDEST: 'drop_oldest',
ERROR: 'error',
});
// How a mount's capacity relates to a df-style report. QUOTA: real
// total/used/available are known (a real filesystem, a provider that
// exposes a storage quota, or a mount's capacity). ELASTIC: no fixed size
// (object stores that grow without a quota). NA: no filesystem-capacity
// concept (message/table surfaces). UNKNOWN: bounded but not cheaply
// measurable / not reported yet.
// df renders real numbers for QUOTA and a literal `-` for the rest — never a
// fabricated total.
export const CapacityState = {
QUOTA: 'quota',
ELASTIC: 'elastic',
NA: 'na',
UNKNOWN: 'unknown',
};
export class PathSpec {
virtual;
directory;
vfsPath;
pattern;
resolved;
// The word's spelling: as typed for relative words, the absolute path
// for everything else (defaults to `virtual`).
rawPath;
// Absolute spelling before dot normalization; excluded from identity.
dotted;
// What the kernel walk already answered for an operand it cannot
// resolve at all, known before the command runs: ENOENT for the empty
// name, whose `virtual` reads as the working directory, and ELOOP for
// one a symlink loop stands in, which `followPaths` leaves unrewritten.
// Every op that reaches it refuses (`walkRefusal`), so each command
// words the refusal as its own. Mirrors Python's PathSpec.walk_error.
walkError;
constructor(init) {
this.virtual = init.virtual;
this.directory = init.directory;
this.vfsPath = init.vfsPath;
this.pattern = init.pattern ?? null;
this.resolved = init.resolved ?? true;
this.rawPath = init.rawPath ?? init.virtual;
this.dotted = init.dotted ?? null;
this.walkError = init.walkError ?? null;
Object.freeze(this);
}
// Mount-relative path with a leading slash. Pure formatting of
// `vfsPath` ('' -> '/', 'sub/x' -> '/sub/x'); used for
// byte-accounting keys and path arithmetic in slash-framed
// mount-relative space.
get mountPath() {
return `/${this.vfsPath}`;
}
get dir() {
// The directory's vfsPath is its virtual form with this path's
// mount prefix removed; the prefix length is recovered from the
// (virtual, vfsPath) pair. Idempotent for specs that are already
// directories.
const cut = rstripSlash(this.virtual).length - this.vfsPath.length;
return new PathSpec({
virtual: this.directory,
directory: this.directory,
vfsPath: stripSlash(this.directory.slice(cut)),
pattern: this.pattern,
resolved: false,
});
}
child(name) {
return `${rstripSlash(this.virtual)}/${name}`;
}
/** Join components for workspace dispatch, preserving the unproven walk. */
join(...parts) {
let word = '';
for (const part of parts)
word = part.startsWith('/') ? part : word ? `${word}/${part}` : part;
const path = PathSpec.fromStrPath(word || '.', undefined, this);
return new PathSpec({
virtual: path.virtual,
directory: path.directory,
vfsPath: path.vfsPath,
rawPath: path.dotted ?? path.virtual,
dotted: path.dotted,
walkError: path.walkError,
});
}
/** Lexical parent with its spelled ancestors, for workspace dispatch. */
get parent() {
const spelled = rstripSlash(this.dotted ?? this.virtual);
return PathSpec.fromStrPath(spelled.slice(0, spelled.lastIndexOf('/')) || '/', undefined, '/');
}
/**
* Wrap a resolved path, or resolve a supplied spelling against cwd.
* Existing specs retain their metadata; vfsPath can override the backend key.
* Supply cwd for relative strings, dotted walks and inherited walk errors;
* omit it for an already resolved virtual path.
*/
static fromStrPath(path, vfsPath, cwd) {
if (path instanceof PathSpec) {
return vfsPath === undefined
? path
: new PathSpec({
virtual: path.virtual,
directory: path.directory,
vfsPath,
rawPath: path.rawPath,
pattern: path.pattern,
resolved: path.resolved,
dotted: path.dotted,
walkError: path.walkError,
});
}
const base = cwd instanceof PathSpec ? (cwd.dotted ?? cwd.virtual) : cwd;
const virtual = base === undefined ? path : resolvePath(path, base);
let dotted = null;
let walkError = null;
if (base !== undefined) {
dotted =
cwd instanceof PathSpec && cwd.dotted !== null
? dottedSpelling(path.startsWith('/') ? path : `${base}/${path}`)
: dottedSpelling(path, base);
if (path === '')
walkError = 'ENOENT';
else if (cwd instanceof PathSpec && !path.startsWith('/'))
walkError = cwd.walkError;
}
return new PathSpec({
virtual,
directory: virtual.slice(0, virtual.lastIndexOf('/') + 1) || '/',
vfsPath: vfsPath ?? stripSlash(virtual),
rawPath: path,
dotted,
walkError,
});
}
}
// Shell-text form of an argv word. Text words pass through; paths render
// as spelled (`rawPath`). Use wherever a word re-enters string space (env
// values, function args, the argv text view). Mount I/O keeps using
// `virtual`.
export function wordText(word) {
return word instanceof PathSpec ? word.rawPath : word;
}
//# sourceMappingURL=types.js.map