UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

104 lines • 4.3 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 { IOResult } from "../io/types.js"; import { UsageError } from "../commands/errors.js"; import { byteView, fromByteView, textView } from "../shell/bytes.js"; import { StreamRefusal } from "./errors.js"; import { PARSE_KEPT_CHARS, PARSES_KEPT } from "../commands/spec/constants.js"; import { openStreamSync } from "./stream.js"; const decoder = new TextDecoder(); // What each command's scan answered, by tape; see `scan`. export const SCANS = new Map(); /** * A command's typed words, read by mirage-rs before any input is read. The * tape holds each option as `[dest, value]` in the order typed, a switch with * an empty value. A grammar that reads its operands also gets each operand as * `[OPERAND, word]` where it was typed, a glob already expanded into its * matches in its place. A value the command refuses never reaches an input. * Returns the values keyed as the command's grammar answers; throws * StreamRefusal for the first word the command refuses, with the command's * exit status. * * A tape's answer is kept, per command, as a spec keeps its parses * (PARSES_KEPT, PARSE_KEPT_CHARS), so the admission gate and the handler * reading one line's options share one scan; each entry counts its name, its * value and one more toward PARSE_KEPT_CHARS, so a run of switches is bounded * too. It is kept as the text mirage-rs answered and decoded per reader, so no * reader sees another's changes; a refusal is kept as a copy never thrown, as * Python keeps no traceback of the refused call. Mirrors Python's `scan`. */ export function scan(command, options) { const tape = JSON.stringify(options); let answers; let kept; const chars = options.reduce((sum, [name, text]) => sum + 1 + name.length + text.length, 0); if (chars <= PARSE_KEPT_CHARS) { answers = SCANS.get(command); if (answers === undefined) { answers = new Map(); SCANS.set(command, answers); } kept = answers.get(tape); } if (kept === undefined) { try { const stream = openStreamSync('scan', { command, options: options.map(([name, text]) => [name, byteView(text)]), }); try { kept = decoder.decode(stream.finish()); } finally { stream.close(); } } catch (err) { if (!(err instanceof StreamRefusal)) throw err; kept = new StreamRefusal(err.message, err.output, err.status); } if (answers !== undefined) { if (answers.size >= PARSES_KEPT) answers.clear(); answers.set(tape, kept); } } if (kept instanceof StreamRefusal) throw new StreamRefusal(kept.message, kept.output, kept.status); return JSON.parse(kept); } /** * `scan` for a command whose refusals travel as usage errors: throws * UsageError for the first value the command refuses, in its words and with * its exit status. Mirrors Python's `scanned`. */ export function scanned(command, options) { try { return scan(command, options); } catch (err) { if (!(err instanceof StreamRefusal)) throw err; throw new UsageError(textView(err.message), err.status); } } /** A scan's refusal as the command's result. Mirrors Python's `refused`. */ export function refused(refusal) { return new IOResult({ exitCode: refusal.status, stderr: fromByteView(`${refusal.message}\n`), }); } //# sourceMappingURL=scan.js.map