@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
202 lines • 8.35 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 {} from 'zod';
import { isStdin, stdinStream } from "../utils/stream.js";
import { StreamHost, hostedStream } from "../utils/stream_host.js";
import { specOf } from "../../spec/builtins.js";
import { FlagView } from "../../spec/flag_view.js";
import { stripSlash } from "../../../utils/slash.js";
import { fsStrerror, isFsError } from "../../../errors/fs.js";
import { resolvePath } from "../../../utils/path.js";
import { IOResult, materialize } from "../../../io/types.js";
import { LineFacts, PathSpec } from "../../../types.js";
import { SED_MISSING_SCRIPT } from "../constants.js";
import { byteView, encodeText, utf8Locale } from "../../../shell/bytes.js";
import { posixPhrase } from "../../../errors/posix.js";
import { openStreamSync } from "../../../rs/stream.js";
function failed(stderr, exitCode) {
return [null, new IOResult({ exitCode, stderr: encodeText(stderr) })];
}
/**
* Which -e/-f occurrences of the line were script files, and their names
* as spelled, for GNU's `file NAME line N:` diagnostics. The executor
* reads a script file before sed runs and hands its text on as one more
* -e (so every sub-run of a fanned-out line sees the same program), which
* leaves only the line's own words to tell the two apart. `null` for an
* expression; the whole answer is null when the words are not the line's
* (a split run) or do not account for every occurrence.
*/
function scriptOrigins(argv, count) {
if (argv === undefined)
return null;
const out = [];
for (let i = 0; i < argv.length; i++) {
const word = argv[i] ?? '';
if (word === '--')
break;
if (word.startsWith('--')) {
if (word === '--line-length')
i += 1;
continue;
}
if (!word.startsWith('-') || word === '-')
continue;
for (let j = 1; j < word.length; j++) {
const c = word.charAt(j);
if (c !== 'e' && c !== 'f' && c !== 'l')
continue;
let value = word.slice(j + 1);
if (value === '') {
i += 1;
value = argv[i] ?? '';
}
if (c !== 'l')
out.push(c === 'f' ? value : null);
break;
}
}
return out.length === count ? out : null;
}
function parseFlags(bag) {
const fl = new FlagView(bag, specOf('sed'));
return {
inPlace: fl.asBool('i'),
suppress: fl.asBool('n'),
extended: fl.asBool('E') || fl.asBool('r'),
separate: fl.asBool('separate'),
lineLength: fl.asText('line_length') ?? null,
scripts: fl.occurrences('e', 'f').map(([name, value]) => [name, String(value)]),
};
}
/**
* A sed line may change files under `-i`, with a script file (its text is
* read only when sed runs), or when its script names a `w` file, which GNU
* opens even when it then refuses the script. The first of `operands` is
* the script when no `-e` or `-f` gave one. Mirrors Python's `facts`.
*/
export function facts(fl, operands) {
if (fl.asBool('i') || fl.raw('f') !== undefined)
return new LineFacts();
const expressions = fl.asList('e');
const scripts = expressions.length > 0
? expressions
: operands.slice(0, 1).map((w) => (typeof w === 'string' ? w : w.rawPath));
if (scripts.length === 0)
return new LineFacts({ writes: false });
return new LineFacts({ writes: scriptWrites(scripts, fl.asBool('E') || fl.asBool('r')) });
}
const writeChecks = new Map();
function scriptWrites(scripts, extended) {
const cacheable = scripts.reduce((size, script) => size + script.length + 1, 0) <= 4096;
const key = cacheable ? JSON.stringify([scripts, extended]) : null;
if (key !== null) {
const known = writeChecks.get(key);
if (known !== undefined) {
writeChecks.delete(key);
writeChecks.set(key, known);
return known;
}
}
const engine = openStreamSync('sed', {
pieces: scripts.map((text) => ({ text: byteView(text) })),
extended,
});
try {
const writes = JSON.parse(new TextDecoder().decode(engine.metadata()))
.writes;
if (key !== null) {
if (writeChecks.size >= 64) {
const oldest = writeChecks.keys().next().value;
if (oldest !== undefined)
writeChecks.delete(oldest);
}
writeChecks.set(key, writes);
}
return writes;
}
finally {
engine.close();
}
}
/**
* Run sed over the resolved operands, as GNU sed 4.9. The script comes from
* -e expressions and -f script files, compiled in option order, when any
* were given, otherwise from the first positional operand. mirage-rs
* compiles and runs it; every operand and every file the script names is a
* request the host serves. Mirrors Python's sed_generic.
*/
export async function sedGeneric(paths, texts, ctx, stream) {
const parsed = parseFlags(ctx.flags);
const scriptStream = parsed.inPlace ? stream : stdinStream(stream, ctx.stdin);
const pieces = [];
const origins = scriptOrigins(ctx.argv, parsed.scripts.length);
for (const [index, [name, text]] of parsed.scripts.entries()) {
const shown = origins?.[index] ?? null;
if (name === 'e') {
pieces.push(shown === null ? { text: byteView(text) } : { text: byteView(text), file: byteView(shown) });
continue;
}
let body;
try {
body = await materialize(scriptStream(PathSpec.fromStrPath(text)));
}
catch (err) {
if (!isFsError(err))
throw err;
const strerror = fsStrerror(err) ?? posixPhrase('EACCES');
return failed(`sed: couldn't open file ${shown ?? text}: ${strerror}\n`, 4);
}
pieces.push({ text: byteView(body), file: byteView(shown ?? text) });
}
if (pieces.length === 0 && texts[0] !== undefined)
pieces.push({ text: byteView(texts[0]) });
if (pieces.length === 0)
return failed(`${SED_MISSING_SCRIPT}\n`, 1);
// -i edits `-` as a file; otherwise an operand naming stdin is `-` to F
// and to sed's messages.
const names = paths.map((p) => (!parsed.inPlace && isStdin(p) ? '-' : p.rawPath));
const config = {
pieces,
extended: parsed.extended,
utf8: utf8Locale(ctx.env),
quiet: parsed.suppress,
separate: parsed.separate,
in_place: parsed.inPlace,
line_length: parsed.lineLength === null ? null : byteView(parsed.lineLength),
inputs: names.map((name) => byteView(name)),
paths: paths.map((p) => byteView(p.virtual)),
cwd: byteView(ctx.cwd.virtual),
};
const host = new StreamHost(paths, stream, ctx, !parsed.inPlace);
const io = new IOResult();
return [hostedStream('sed', config, host, io, ctx.signal), io];
}
/**
* When the script is supplied via -e/-f, GNU sed treats every bare argument as
* a file. The arg parser instead routes the first bare arg into the positional
* `text` (script) slot, so recover it as a path operand here.
*/
export function positionalAsPaths(texts, ctx) {
return texts.map((t) => {
const resolved = resolvePath(t, ctx.cwd.virtual);
const slash = resolved.lastIndexOf('/');
return new PathSpec({
virtual: resolved,
directory: slash >= 0 ? resolved.slice(0, slash + 1) : '/',
resolved: true,
vfsPath: stripSlash(resolved),
});
});
}
//# sourceMappingURL=sed.js.map