UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

238 lines • 10.1 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. export { PipeClosed } from "../io/errors.js"; import { encodeText } from "./bytes.js"; // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= /** * A bash arithmetic syntax or evaluation error, worded as bash's line: the * expression, what went wrong, and the text from the token the reader * stood on to the end (`1+: syntax error: operand expected (error token is * "+")`). `writes` carries the assignments the expression made before it * failed: bash binds each at once, so `x=5, 1/0` leaves `x` at 5 and * `RANDOM=42, RANDOM + 1/0` leaves the generator seeded and drawn from. * The evaluator fills it as it throws; a caller lands them the way it * lands a successful result's, then reports the error. `inSubscript` * marks one made while an array subscript evaluated, which ends the shell * wherever the subscript is (`let 'a[1+]'`), where `let` would otherwise * fail with 1. Mirrors Python's mirage.shell.errors.ArithError. */ export class ArithError extends Error { reason; expression; token; writes = []; inSubscript = false; constructor(reason, expression = '', token = null) { let line = expression === '' ? reason : `${expression}: ${reason}`; if (token !== null) line = `${line} (error token is "${token}")`; super(line); this.name = 'ArithError'; this.reason = reason; this.expression = expression; this.token = token; } /** * How the error unwinds where no status answers it: one in a subscript, * or in an `-i` value (`fatal`), ends the shell with 1; any other * discards the line, as `$((1/0))` does. The command the error belongs * to leads the line (`bash: read: 1+: ...`, `bash: x: 1/0: ...` for * `${x:1/0}`), except one in a subscript, which bash names by the * subscript alone. */ signal(cmd = '', fatal = false) { const lead = cmd !== '' && !this.inSubscript ? `${cmd}: ` : ''; const stderr = encodeText(`bash: ${lead}${this.message}\n`); if (fatal || this.inSubscript) return new ExitSignal(1, stderr, null, 1); return new DiscardSignal(stderr); } } /** * An assignment to a readonly shell variable: a session write to a name * `readonly` marked, or one an arithmetic expression made. Its message is * bash's `<name>: readonly variable`, which each caller renders in its own * voice. An arithmetic evaluation stops at it, as bash's does: `writes` * carries the assignments made before it, which bind (`(( X=5, R=3 ))` * leaves X at 5), and nothing after it runs. `inSubscript` marks one made * while an array subscript evaluated (`${a[R=3]}`, `(( a[R=3] ))`), which * ends the shell wherever the subscript is. Mirrors Python's * mirage.shell.errors.ReadonlyVariableError. */ export class ReadonlyVariableError extends Error { varName; inSubscript; writes = []; constructor(name, inSubscript = false) { super(`${name}: readonly variable`); this.name = 'ReadonlyVariableError'; this.varName = name; this.inSubscript = inSubscript; } /** * How the error unwinds where no status answers it: one in a subscript, * or in an `-i` value (`fatal`), ends the shell with 1; any other * discards the line, as `$((R=3))` does. */ signal(fatal = false) { const stderr = encodeText(`bash: ${this.message}\n`); if (fatal || this.inSubscript) return new ExitSignal(1, stderr, null, 1); return new DiscardSignal(stderr); } } // A fatal shell exit request unwinding the current execution. Raised by // the `exit` builtin and by fatal expansion errors (`${var:?msg}`), which // bash treats as an implicit `exit 1` in a non-interactive shell. // Contained at subshell, pipeline-segment, and background-job boundaries; // the top-level program loop stops the remaining statements. Mirrors // Python's mirage.shell.errors.ExitSignal. export class ExitSignal extends Error { exitCode; stderr; stdout; // Status a containing boundary reports instead of exitCode. GNU bash // exits 127 on a fatal expansion error but a subshell wrapping one // returns 1; `exit N` uses N in both positions (the default). containedCode; // The id of the command whose own words were being expanded when it was // raised. bash expands a simple command's words before it applies the // command's redirects, so that diagnostic goes around them; any other // goes through the redirects it was written under. expanding = null; // The program an `exec` replaced the shell with, whose actions went with // it. replaced = null; // Whether stdout is a nested line's (an `exec`'d program's, or an ERR or // RETURN action's that left), which the redirects the signal unwinds // through still route. An EXIT action's output goes around those // redirects, as bash runs it once the shell has unwound. unrouted = false; // That EXIT output, the end of stdout. cleanup = new Uint8Array(); // Whether it was raised in text `eval` or `source` ran: a forked stage or // job reports its contained status even for a simple command // (`eval ': ${U?}' | cat` is 1, `: ${U?} | cat` 127). sourced = false; constructor(exitCode = 0, stderr = new Uint8Array(), stdout = null, containedCode = null) { super('exit'); this.name = 'ExitSignal'; this.exitCode = exitCode; this.stderr = stderr; this.stdout = stdout; this.containedCode = containedCode ?? exitCode; } } /** * An error after which bash discards the rest of the line. A bad * substitution, an arithmetic or assignment error, a write the shell refuses: * the command never runs, and neither do the statements after it on its line, * but the next line does, with `$?` at 1. The line loop of a shell, of `eval` * and of `source` resumes there; a child shell ends on it with status 1, and * so does `set -e`. `containedCode` is the status a `( )` subshell, or a * compound command forked as a stage or job, ends with when the error * reaches it rather than a line loop: 2 for a refused `${var:=word}`, 1 for * any other. Mirrors Python's mirage.shell.errors.DiscardSignal. */ export class DiscardSignal extends ExitSignal { constructor(stderr = new Uint8Array(), containedCode = 1) { super(1, stderr, null, containedCode); this.name = 'DiscardSignal'; } } /** * A glob that matches more paths than its mount's `walkLimit`. A command * it is a word of does not run, as exec(2) refuses an argument list too * long; anywhere else (a `for` list, a redirect) the line is discarded like * any expansion error. `reason` is the limit's line. Mirrors Python's * mirage.shell.errors.TooManyMatches. */ export class TooManyMatches extends DiscardSignal { reason; constructor(reason) { super(encodeText(`bash: ${reason}\n`)); this.name = 'TooManyMatches'; this.reason = reason; } } /** * `set -u` reading a name that is not set: `$x`, `${a[i]}`, or a variable * an arithmetic expression reads. GNU bash dies on it the way it dies on * `${x:?}`: status 127 at top level, 1 from a containing subshell or * pipeline segment. Mirrors Python's mirage.shell.errors.UnboundVariable. */ export class UnboundVariable extends ExitSignal { constructor(name) { super(127, encodeText(`bash: ${name}: unbound variable\n`), null, 1); this.name = 'UnboundVariable'; } } /** * A `${...}` bash cannot read, found as its word expands. bash names the text * of the expansion it was running: the whole word, a double-quoted part's * inside, an operator's word, an arithmetic expression, a heredoc's body. * Each level the error leaves renames it (`within`) until one of those fixes * the name. Mirrors Python's mirage.shell.errors.BadSubstitution. */ export class BadSubstitution extends DiscardSignal { fixed = false; constructor(text) { super(); this.name = 'BadSubstitution'; this.within(text); } /** Name the word being expanded, unless a boundary already has. */ within(word, fixed = false) { if (!this.fixed) { this.stderr = encodeText(`bash: ${word}: bad substitution\n`); this.fixed = fixed; } return this; } } /** * Await an expansion of `word`, which a bad substitution names: a * double-quoted part's inside, an operator's word, an arithmetic expression. * Mirrors Python's mirage.shell.errors.named. */ export async function named(word, pending) { try { return await pending; } catch (err) { if (err instanceof BadSubstitution) throw err.within(word, true); throw err; } } /** * `return` unwinding to the function or sourced file it ends; `stdout` is the * output the constructs it left had produced before it. */ export class ReturnSignal extends Error { exitCode; stderr; stdout; // Whether stdout is an ERR or RETURN action's that left with `return`, // which the redirects it unwinds through still route, as // `ExitSignal.unrouted` does. unrouted = false; constructor(exitCode, stderr = new Uint8Array(), stdout = null) { super('return'); this.name = 'ReturnSignal'; this.exitCode = exitCode; this.stderr = stderr; this.stdout = stdout; } } //# sourceMappingURL=errors.js.map