@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
513 lines • 21 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 { chunks } from "../../../io/cooperative.js";
import { PipeClosed } from "../../../io/errors.js";
import { DiffPool } from "../../../rs/diff_pool.js";
import { StreamRefusal } from "../../../rs/errors.js";
import { byteView } from "../../../shell/bytes.js";
import { refused } from "../../../rs/scan.js";
import { openStreamSync } from "../../../rs/stream.js";
import { compileSpec } from "../../spec/compile.js";
import { specOf } from "../../spec/builtins.js";
import { FlagView } from "../../spec/flag_view.js";
import { mountKey, mountPrefixOf } from "../../../utils/key_prefix.js";
import { concat } from "../../../utils/bytes.js";
import { IOResult, materialize } from "../../../io/types.js";
import { FileType, PathSpec } from "../../../types.js";
import { gnuBasename } from "../../../utils/path.js";
import { rstripSlash } from "../../../utils/slash.js";
import { formatFsError } from "../../../errors/render.js";
import { CommandTimeoutError } from "../../../errors/types.js";
import { fsStrerror, isEnoent, isFsError } from "../../../errors/fs.js";
import { posixPhrase } from "../../../errors/posix.js";
import { followedStat } from "../../../view/namespace_view.js";
import { extraOperandError, missingOperandError } from "../../spec/usage.js";
import { isStdin, stdinStat, stdinStream } from "../utils/stream.js";
import { UsageError } from "../../errors.js";
import { beforeFullWalk } from "../utils/tree.js";
import { CommandName } from "../../spec/types.js";
import { compareCodePoints } from "../../../utils/sort.js";
import { fnmatch } from "../../../utils/fnmatch.js";
import { shellQuote } from "../../../utils/quote.js";
import { fullIsoTime } from "../utils/formatting.js";
import { UTC_ZONE, zoneFromEnv } from "../../../utils/timezone.js";
import { linksOf, dispatchOf } from "../utils/links.js";
import { vfsStatPath } from "../utils/paths.js";
import { systemDate } from "../../../utils/clock.js";
const ENC = new TextEncoder();
const DEC = new TextDecoder('utf-8', { fatal: false });
// The context -U and --unified give, read by the diff engine: each value as
// typed, null for a bare --unified. Mirrors Python's `_unified_context`.
function unifiedContext(typed) {
if (typed.length === 0)
return 3;
let engine;
try {
engine = openStreamSync('diff', {
unified: typed.map((text) => (text === null ? null : byteView(text))),
});
}
catch (err) {
if (!(err instanceof StreamRefusal))
throw err;
return refused(err);
}
try {
const { context } = JSON.parse(DEC.decode(engine.metadata()));
return context;
}
finally {
engine.close();
}
}
/**
* diff's options, -U refused by the diff engine before any operand: the
* flags, or diff's refusal of a -U, exit 2. Mirrors Python's `parse_flags`.
*/
export function parseFlags(bag) {
const fl = new FlagView(bag, specOf('diff'));
const typed = fl
.occurrences('U', 'unified')
.map(([, value]) => (value === true ? null : String(value)));
const context = unifiedContext(typed);
if (context instanceof IOResult)
return context;
const newFile = fl.asBool('new_file');
return {
ignoreCase: fl.asBool('i'),
ignoreAllSpace: fl.asBool('w'),
ignoreSpaceChange: fl.asBool('b'),
ed: fl.asBool('e'),
unified: fl.asBool('u') || typed.length > 0,
context,
brief: fl.asBool('brief'),
recursive: fl.asBool('recursive'),
newFile,
newFirst: newFile || fl.asBool('unidirectional_new_file'),
identical: fl.asBool('report_identical_files'),
exclude: fl.asList('exclude'),
excludeFrom: fl.asPaths('exclude_from'),
};
}
const PRESENT = [false, false];
// The characters GNU diff spells as a C escape in a header name. Any other
// control character is three octal digits.
const HEADER_ESCAPES = {
'\x07': 'a',
'\b': 'b',
'\t': 't',
'\n': 'n',
'\v': 'v',
'\f': 'f',
'\r': 'r',
'"': '"',
'\\': '\\',
};
/**
* A file name as GNU diff writes it in a header line. GNU diff double-quotes
* a name holding a space, a double quote, a backslash or a control
* character, and writes each of those but the space as a C escape
* (`"sp ace"`, `"t\\tab"`); any other name, bytes above ASCII included, is
* written as it is. The `---` and `+++` lines and the `diff -r` line use it;
* the `Only in` and `Files ... differ` lines do not. Mirrors Python's
* `quote_header_name`.
*/
export function quoteHeaderName(name) {
const special = (ch) => ch === ' ' || Object.hasOwn(HEADER_ESCAPES, ch) || ch < ' ';
if (!Array.from(name).some(special))
return name;
let out = '';
for (const ch of name) {
const escape = Object.hasOwn(HEADER_ESCAPES, ch) ? HEADER_ESCAPES[ch] : undefined;
if (escape !== undefined)
out += '\\' + escape;
else if (ch < ' ')
out += '\\' + ch.charCodeAt(0).toString(8).padStart(3, '0');
else
out += ch;
}
return `"${out}"`;
}
// The time a unified header gives one side, as GNU diff prints it: the
// modification time as `%Y-%m-%d %H:%M:%S.%N %z` in the zone `TZ` names, read
// through the dispatcher as `stat` reads it, so a time `touch` keeps in the namespace shows; the
// epoch for a side -N reads as absent; and the present moment for standard
// input, as POSIX asks and diffutils does.
async function headerTime(walk, path, absent) {
if (absent)
return fullIsoTime(null, walk.zone);
if (isStdin(path))
return fullIsoTime(systemDate().toISOString(), walk.zone);
const info = (walk.statPath !== null ? await walk.statPath(path) : null) ?? (await walk.stat(path));
return fullIsoTime(info.modified, walk.zone);
}
function childSpec(parent, name) {
const childPath = `${rstripSlash(parent.virtual)}/${name}`;
return new PathSpec({
virtual: childPath,
directory: childPath,
resolved: false,
vfsPath: mountKey(childPath, mountPrefixOf(parent.virtual, parent.vfsPath)),
rawPath: `${rstripSlash(parent.rawPath)}/${name}`,
});
}
function takesValue(option) {
return option.action !== 'store_true' && option.action !== 'count' && !(option.nargs === '?');
}
/**
* The option words of a diff line, as GNU echoes them. GNU diff permutes its
* options in front of its operands and prints them, word for word and in
* order, on each `diff -r` header line: an option's detached value is its
* own word, `--` is kept, and every operand is left out.
*/
export function switchWords(argv) {
const options = compileSpec(specOf(CommandName.DIFF)).options;
const shorts = new Map(options.flatMap((o) => o.names.filter((name) => !name.startsWith('--')).map((name) => [name.slice(1), o])));
const longs = new Map(options.flatMap((o) => o.names.filter((name) => name.startsWith('--')).map((name) => [name.slice(2), o])));
const words = [];
let i = 0;
while (i < argv.length) {
const word = argv[i] ?? '';
i += 1;
if (word === '--') {
words.push(word);
break;
}
if (!word.startsWith('-') || word === '-')
continue;
words.push(word);
if (word.startsWith('--')) {
const name = word.slice(2).split('=')[0] ?? '';
const found = longs.get(name);
const matches = found !== undefined
? [found]
: [...longs].filter(([n]) => n.startsWith(name)).map(([, o]) => o);
const only = matches.length === 1 ? matches[0] : undefined;
if (!word.includes('=') && only !== undefined && takesValue(only) && i < argv.length) {
words.push(argv[i] ?? '');
i += 1;
}
continue;
}
for (let at = 1; at < word.length; at++) {
const option = shorts.get(word[at] ?? '');
if (option !== undefined && takesValue(option)) {
if (at === word.length - 1 && i < argv.length) {
words.push(argv[i] ?? '');
i += 1;
}
break;
}
}
}
return words;
}
async function diffPair(walk, path1, path2, absent = PRESENT) {
const flags = walk.flags;
const headers = flags.unified && !flags.brief && !flags.ed
? {
old_header: `${quoteHeaderName(path1.rawPath)}\t${await headerTime(walk, path1, absent[0])}`,
new_header: `${quoteHeaderName(path2.rawPath)}\t${await headerTime(walk, path2, absent[1])}`,
}
: {};
const result = await walk.computer.compare({
mode: flags.ed ? 'ed' : flags.unified ? 'unified' : 'normal',
context: flags.context,
ignore_case: flags.ignoreCase,
ignore_all_space: flags.ignoreAllSpace,
ignore_space_change: flags.ignoreSpaceChange,
text: false,
brief: flags.brief,
old_label: path1.rawPath,
new_label: path2.rawPath,
old_header: headers.old_header ?? '',
new_header: headers.new_header ?? '',
}, () => (absent[0] ? new Uint8Array() : walk.stream(path1)), () => (absent[1] ? new Uint8Array() : walk.stream(path2)), walk.signal);
if (result.outcome === 'equal' && flags.identical && result.status === 0)
return {
...result,
output: [ENC.encode(`Files ${path1.rawPath} and ${path2.rawPath} are identical\n`)],
};
if (result.outcome === 'different' && flags.brief)
return {
...result,
output: [ENC.encode(`Files ${path1.rawPath} and ${path2.rawPath} differ\n`)],
};
return result;
}
async function entries(walk, path, absent) {
if (absent)
return new Set();
const names = (await walk.readdir(path)).map((e) => gnuBasename(e));
return new Set(names.filter((name) => !walk.excluded.some((pattern) => fnmatch(name, pattern))));
}
// What an entry is, as diff's stat follows it; null, and the entry named in
// `trouble`, for one it cannot stat or a link that leads nowhere. GNU names
// each such entry on each side and goes on with the rest, the pair left
// uncompared (exit 2). Mirrors Python's _entry_kind.
async function entryKind(walk, path) {
let row;
try {
row = await walk.stat(path);
}
catch (err) {
if (!isFsError(err))
throw err;
walk.trouble.push(ENC.encode(`diff: ${path.rawPath}: ${String(fsStrerror(err))}\n`));
return null;
}
if (row.type === FileType.SYMLINK) {
walk.trouble.push(ENC.encode(`diff: ${path.rawPath}: ${posixPhrase('ENOENT')}\n`));
return null;
}
return row.type;
}
/**
* Compare two directories, one of which -N may read as empty; returns the
* report and whether any pair differed. Where diff follows links, a pair whose
* every side is a directory already open above it on that side (`above`, as
* the paths they resolve to) is GNU's recursive directory loop: named and left
* unread (exit 2).
*/
async function diffDirs(walk, dirA, dirB, absent = PRESENT, above = [[], []]) {
const flags = walk.flags;
const real = walk.real;
const opened = real === null
? above
: [
absent[0] ? above[0] : [...above[0], real(dirA.virtual)],
absent[1] ? above[1] : [...above[1], real(dirB.virtual)],
];
const namesA = await entries(walk, dirA, absent[0]);
const namesB = await entries(walk, dirB, absent[1]);
const names = [...new Set([...namesA, ...namesB])].sort(compareCodePoints);
const left = rstripSlash(dirA.rawPath);
const right = rstripSlash(dirB.rawPath);
const parts = [];
let differ = false;
for (const name of names) {
const inA = namesA.has(name);
const inB = namesB.has(name);
if (!inB && !flags.newFile) {
parts.push(ENC.encode(`Only in ${left}: ${name}\n`));
differ = true;
continue;
}
if (!inA && !flags.newFirst) {
parts.push(ENC.encode(`Only in ${right}: ${name}\n`));
differ = true;
continue;
}
const childA = childSpec(dirA, name);
const childB = childSpec(dirB, name);
const kindA = inA ? await entryKind(walk, childA) : null;
const kindB = inB ? await entryKind(walk, childB) : null;
if ((inA && kindA === null) || (inB && kindB === null))
continue;
const aDir = kindA === FileType.DIRECTORY;
const bDir = kindB === FileType.DIRECTORY;
const gone = [!inA, !inB];
if ((aDir || !inA) && (bDir || !inB)) {
if (real !== null &&
[childA, childB].every((child, i) => gone[i] === true || opened[i]?.includes(real(child.virtual)) === true)) {
const named = inA ? childA : childB;
walk.trouble.push(ENC.encode(`diff: ${named.rawPath}: recursive directory loop\n`));
continue;
}
const [body, changed] = await diffDirs(walk, childA, childB, gone, opened);
for (const chunk of body)
parts.push(chunk);
differ ||= changed;
}
else if (!aDir && !bDir) {
const pair = await diffPair(walk, childA, childB, gone);
for (const chunk of pair.stderr)
walk.trouble.push(chunk);
if (pair.status === 2 && pair.stderr.length === 0)
walk.trouble.push(ENC.encode('diff: comparison failed\n'));
if (pair.outcome === 'equal') {
for (const chunk of pair.output)
parts.push(chunk);
continue;
}
differ = true;
if (!flags.brief)
parts.push(ENC.encode(`diff${walk.switches} ${quoteHeaderName(childA.rawPath)} ${quoteHeaderName(childB.rawPath)}\n`));
for (const chunk of pair.output)
parts.push(chunk);
}
else if (aDir) {
differ = true;
parts.push(ENC.encode(`File ${childA.rawPath} is a directory while file ${childB.rawPath} is a regular file\n`));
}
else {
differ = true;
parts.push(ENC.encode(`File ${childA.rawPath} is a regular file while file ${childB.rawPath} is a directory\n`));
}
}
return [parts, differ];
}
async function missing(stat, path, allowed) {
if (!allowed || isStdin(path))
return false;
try {
await stat(path);
}
catch (err) {
if (isEnoent(err))
return true;
throw err;
}
return false;
}
async function excludedPatterns(flags, stream) {
const patterns = [...flags.exclude];
for (const path of flags.excludeFrom) {
const text = DEC.decode(await materialize(stream(path)));
for (const line of text.split('\n'))
if (line !== '')
patterns.push(line);
}
return patterns;
}
export async function diffGeneric(flags, paths, ctx, read, readdir, backendStat) {
if (paths.length > 2)
throw extraOperandError(CommandName.DIFF, paths[2]?.rawPath ?? '');
if (paths.length < 2)
throw missingOperandError(CommandName.DIFF, paths[0]?.rawPath ?? null, ctx.argv ?? []);
const p0 = paths[0];
const p1 = paths[1];
if (p0 === undefined || p1 === undefined)
return [null, new IOResult()];
// Both name the one stdin, which GNU sees as the same file.
if (isStdin(p0) && isStdin(p1))
return [null, new IOResult()];
const stream = stdinStream(read, ctx.stdin);
const stat = stdinStat(backendStat);
const dash0 = p0.rawPath === '-';
const dash1 = p1.rawPath === '-';
const errorPaths = [...paths, ...flags.excludeFrom];
const links = linksOf(ctx);
const trouble = [];
let output;
let differ = false;
const owned = ctx.diff === undefined ? new DiffPool() : undefined;
const computer = ctx.diff ?? owned;
if (computer === undefined)
throw new Error('diff computer is missing');
try {
const walk = {
computer,
signal: ctx.signal,
stream,
readdir,
// diff compares what each link leads to (its default --dereference),
// the operands' own taken there at dispatch.
stat: (p) => followedStat(stat, links, p),
flags,
excluded: await excludedPatterns(flags, stream),
switches: switchWords(ctx.argv ?? [])
.map((word) => ` ${shellQuote(word)}`)
.join(''),
statPath: vfsStatPath(ctx.vfs) ?? null,
zone: zoneFromEnv(ctx.env) ?? UTC_ZONE,
trouble,
real: links === undefined ? null : (virtual) => links.resolve(virtual),
};
if (dash0 !== dash1) {
if ((await stat(dash0 ? p1 : p0)).type === FileType.DIRECTORY) {
return [
null,
new IOResult({
exitCode: 2,
stderr: ENC.encode("diff: cannot compare '-' to a directory\n"),
}),
];
}
}
// A missing operand -N reads as empty only beside one that is there:
// two missing ones are both reported.
const absent = [
await missing(stat, p0, flags.newFirst),
await missing(stat, p1, flags.newFile),
];
if (absent[0] && absent[1]) {
const lines = [];
for (const path of paths) {
try {
await stat(path);
}
catch (err) {
if (!isFsError(err))
throw err;
lines.push(formatFsError('diff', err, [path]));
}
}
return [null, new IOResult({ exitCode: 2, stderr: concat(lines) })];
}
if (flags.recursive && !absent[0] && !absent[1]) {
const bothDirs = (await stat(p0)).type === FileType.DIRECTORY && (await stat(p1)).type === FileType.DIRECTORY;
if (bothDirs) {
for (const path of [p0, p1])
await beforeFullWalk(dispatchOf(ctx), path, 'diff');
[output, differ] = await diffDirs(walk, p0, p1);
}
}
if (output === undefined) {
const pair = await diffPair(walk, p0, p1, absent);
output = pair.output;
differ = pair.outcome === 'different';
for (const chunk of pair.stderr)
trouble.push(chunk);
if (pair.status === 2 && pair.stderr.length === 0)
trouble.push(ENC.encode('diff: comparison failed\n'));
}
}
catch (err) {
if (err instanceof StreamRefusal)
return [
err.output,
new IOResult({ exitCode: 2, stderr: ENC.encode(`diff: ${err.message}\n`) }),
];
if (!isFsError(err)) {
if (err instanceof UsageError ||
err instanceof CommandTimeoutError ||
err instanceof PipeClosed ||
(err instanceof Error && err.name === 'AbortError'))
throw err;
return [
null,
new IOResult({
exitCode: 2,
stderr: ENC.encode(`diff: ${err instanceof Error ? err.message : String(err)}\n`),
}),
];
}
// GNU diff reserves exit 1 for "files differ"; trouble (a missing or
// unreadable operand) is exit 2.
return [null, new IOResult({ exitCode: 2, stderr: formatFsError('diff', err, errorPaths) })];
}
finally {
await owned?.close();
}
const out = outputChunks(output);
if (trouble.length > 0)
return [out, new IOResult({ exitCode: 2, stderr: concat(trouble) })];
return [out, new IOResult({ exitCode: differ ? 1 : 0 })];
}
async function* outputChunks(parts) {
for (const part of parts)
yield* chunks(part);
}
//# sourceMappingURL=diff.js.map