UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

250 lines • 9.35 kB
// ========= 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