@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
250 lines • 9.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 { OutputStream } from "../../../io/stream.js";
import { AsyncLineIterator } from "../../../io/async_line_iterator.js";
import { specOf } from "../../spec/builtins.js";
import { OPERAND, SPELLED } from "../../spec/constants.js";
import { FlagView, spreadOperands } from "../../spec/flag_view.js";
import { IOResult } from "../../../io/types.js";
import { UsageError } from "../../errors.js";
import { missingOperandError } from "../../spec/usage.js";
import { CommandName } from "../../spec/types.js";
import { byteView, encodeText } from "../../../shell/bytes.js";
import { closeStream, mergeOutput, openStream } from "../../../rs/stream.js";
import { concat } from "../../../utils/bytes.js";
import { StreamRefusal } from "../../../rs/errors.js";
import { isStdin, neverOpened, stdinStream } from "../utils/stream.js";
import { fsStrerror, isFsError } from "../../../errors/fs.js";
import { fsErrorLine } from "../../../errors/render.js";
const OPTION_DESTS = [
'a',
'v',
'e',
'ignore_case',
'args_1',
'2',
'j',
'o',
't',
'zero_terminated',
'check_order',
'nocheck_order',
'header',
];
/**
* What join does with a line whose key sorts before the one above it. WARN,
* the default, names the first such line of each file, but only once some
* line has gone unpaired, then finishes and exits 1 under `input is not in
* sorted order`. FAIL (`--check-order`) names the first one and stops there.
* OFF (`--nocheck-order`) never looks. Mirrors OrderCheck in join.py.
*/
export var OrderCheck;
(function (OrderCheck) {
OrderCheck["WARN"] = "warn";
OrderCheck["FAIL"] = "fail";
OrderCheck["OFF"] = "off";
})(OrderCheck || (OrderCheck = {}));
function options(flags, operands, argv) {
const tape = new FlagView(flags, specOf(CommandName.JOIN)).occurrences(OPERAND, SPELLED, ...OPTION_DESTS);
return {
options: tape.map(([name, value]) => [
name,
typeof value === 'string' ? byteView(value) : value,
]),
operands: operands.map((word) => byteView(word)),
argv: argv.map((word) => byteView(word)),
};
}
function parsed(stream) {
const config = JSON.parse(new TextDecoder().decode(stream.metadata()));
return {
keys: config.keys,
unpaired: config.unpaired,
paired: config.paired,
filler: config.filler === null ? null : byteView(new Uint8Array(config.filler)),
columns: config.columns,
autoColumns: config.auto_columns,
delimiter: config.delimiter === null ? null : byteView(new Uint8Array(config.delimiter)),
separator: byteView(new Uint8Array(config.separator)),
foldCase: config.fold_case,
terminator: config.zero_terminated ? '\0' : '\n',
order: config.order,
header: config.header,
files: config.files,
};
}
export async function parseFlags(flags, operands = [], argv = []) {
try {
const engine = await openStream('join', options(flags, operands, argv));
try {
return parsed(engine);
}
finally {
await closeStream(engine);
}
}
catch (error) {
if (!(error instanceof StreamRefusal))
throw error;
throw new UsageError(error.message, 1);
}
}
/**
* GNU `join` of two files over already-parsed options: `paths` are the
* operands, of which `flags.files` names the two files. Mirrors join in
* join.py.
*/
export function join(paths, io) {
const p1 = paths[io.flags.files[0]];
const p2 = paths[io.flags.files[1]];
if (p1 === undefined || p2 === undefined) {
throw missingOperandError(CommandName.JOIN, paths.at(-1)?.rawPath ?? null, []);
}
if (p1.rawPath === '-' && p2.rawPath === '-') {
return [
null,
new IOResult({
exitCode: 1,
stderr: new TextEncoder().encode('join: both files cannot be standard input\n'),
}),
];
}
const stream = stdinStream(io.read, io.stdin);
const readers = [new AsyncLineIterator(stream(p1)), new AsyncLineIterator(stream(p2))];
const result = new IOResult();
const produced = joinStream(io.flags, readers, [p1, p2], result, io.engine);
return [
new OutputStream(produced, async () => {
await produced.return(undefined);
if (io.engine !== undefined)
await closeStream(io.engine);
for (const reader of readers)
await reader.discard();
}),
result,
];
}
// join's line for an operand it cannot read: named as a missing one when it
// never opened, else as a read error.
function failure(reader, path, error) {
return neverOpened(reader, path, error)
? fsErrorLine(CommandName.JOIN, path, error)
: `join: read error: ${fsStrerror(error) ?? String(error)}\n`;
}
/**
* Open both files before reading either, as GNU does: a missing FILE2 is
* reported over a FILE1 that cannot be read (`join dir missing`). Stdin is
* already open, so it is never read here: `join dir -` reports the directory
* without waiting on input. Mirrors _open in join.py.
*/
async function openInputs(readers, paths) {
let unreadable = null;
for (const side of [0, 1]) {
if (isStdin(paths[side]))
continue;
try {
await readers[side].peek();
}
catch (error) {
if (!isFsError(error))
throw error;
if (neverOpened(readers[side], paths[side], error)) {
return failure(readers[side], paths[side], error);
}
unreadable ??= [side, error];
}
}
if (unreadable === null)
return '';
const [side, error] = unreadable;
return failure(readers[side], paths[side], error);
}
/**
* Walk the two inputs through the `join` engine; a failed read ends the run
* after what join already printed. Mirrors _join_stream in join.py.
*/
async function* joinStream(flags, readers, paths, io, engine) {
const stream = engine ??
(await openStream('join', {
keys: [...flags.keys],
unpaired: [...flags.unpaired],
paired: flags.paired,
filler: flags.filler,
columns: flags.columns.map((item) => [...item]),
auto_columns: flags.autoColumns,
delimiter: flags.delimiter,
separator: flags.separator,
fold_case: flags.foldCase,
zero_terminated: flags.terminator === '\0',
order: flags.order,
header: flags.header,
names: paths.map((path) => byteView(path.rawPath)),
}));
let failed = 0;
const pull = (side) => {
failed = side === 0 ? 0 : 1;
return readers[failed].readChunk();
};
let error = '';
try {
try {
error = await openInputs(readers, paths);
if (error === '')
yield* mergeOutput(stream, pull);
}
catch (caught) {
if (!isFsError(caught))
throw caught;
error = failure(readers[failed], paths[failed], caught);
}
}
finally {
io.stderr = concat([stream.takeStderr(), encodeText(error)]);
io.exitCode = error === '' ? stream.status() : 1;
await closeStream(stream);
for (const reader of readers)
await reader.discard();
}
}
/**
* The builder's entry point: parse the line's flags, then `join`. Each operand's
* glob expands on its own, so the option loop sees its matches where the
* word was typed (`join -j1 2 *.txt`). Mirrors join_generic in join.py.
*/
export async function joinGeneric(paths, ctx, resolveGlob, read) {
const groups = [];
for (const path of paths)
groups.push(await resolveGlob([path]));
const resolved = groups.flat();
const flags = spreadOperands(ctx.flags, groups.map((group) => group.map((path) => path.rawPath)));
try {
const engine = await openStream('join', options(flags, resolved.map((path) => path.rawPath), ctx.argv ?? []));
try {
const result = join(resolved, { read, stdin: ctx.stdin, flags: parsed(engine), engine });
if (result[0] === null)
await closeStream(engine);
return result;
}
catch (error) {
await closeStream(engine);
throw error;
}
}
catch (error) {
if (!(error instanceof StreamRefusal))
throw error;
throw new UsageError(error.message, 1);
}
}
//# sourceMappingURL=join.js.map