UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

289 lines • 11.2 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 { specOf } from "../../../commands/spec/builtins.js"; import { FlagView } from "../../../commands/spec/flag_view.js"; import { SHELL_SYNOPSES } from "../../../commands/spec/synopsis.js"; import { GlobLimitError } from "../../../errors/types.js"; import { IOResult } from "../../../io/types.js"; import { decodeText, encodeText } from "../../../shell/bytes.js"; import { TooManyMatches } from "../../../shell/errors.js"; import { PathSpec, wordText } from "../../../types.js"; import { resolvePath } from "../../../utils/path.js"; import { expandGlob } from "../../expand/globs.js"; import { ExecutionNode } from "../../types.js"; import { optionError, parseFlags } from "../command/flags.js"; import { COUNT_WORD_RE, IDENTIFIER_RE } from "./constants.js"; import { scopePath } from "./scope.js"; /** * Build the (stream, IOResult, ExecutionNode) triple builtins return. * * @param cmd - command name recorded on the ExecutionNode. * @param init - `out` stdout payload; `exitCode` for both results; `stderr` * error text encoded onto both; `io` a prebuilt IOResult to reuse (e.g. * carrying writes), whose exitCode/stderr are overwritten. */ export function result(cmd, init = {}) { const exitCode = init.exitCode ?? 0; const err = init.stderr !== undefined && init.stderr !== '' ? encodeText(init.stderr) : new Uint8Array(); const io = init.io ?? new IOResult(); io.exitCode = exitCode; if (err.length > 0) io.stderr = err; return [init.out ?? null, io, new ExecutionNode({ command: cmd, exitCode, stderr: err })]; } export function ok(cmd, out) { return result(cmd, { out: out ?? null }); } export function fail(cmd, message, exitCode = 1) { return result(cmd, { exitCode, stderr: message }); } /** * Close an operand loop: exit 1 with joined stderr when any operand failed, * exit 0 otherwise. * * @param cmd - command name. * @param errors - per-operand error messages collected so far. * @param io - prebuilt IOResult to reuse (e.g. carrying writes). */ export function finish(cmd, errors, io) { const carried = io !== undefined ? { io } : {}; if (errors.length > 0) { return result(cmd, { exitCode: 1, stderr: errors.join(''), ...carried }); } return result(cmd, carried); } /** * Parse a builtin's words with its spec, the way getopt_long does: options * may follow operands until `--`, long options take their unique * abbreviations, and a bad one is refused in GNU's words with the `Try` * line. The operands keep the PathSpecs the classifier made. Mirrors * Python's parse_line. * * @param cmd - the builtin's name. * @param args - the classified words after the name. * @param cwd - the session working directory. */ export function parseLine(cmd, args, cwd) { const spec = specOf(cmd); const parsed = parseFlags(args, spec, cmd, cwd); const refused = optionError(cmd, parsed); if (refused !== null) { const [message, code] = refused; return [parsed, new FlagView({}, spec), fail(cmd, decodeText(message), code)]; } return [parsed, new FlagView(parsed.flagKwargs, spec), null]; } /** * A path operand as an absolute virtual path. * * @param arg - a classified command part. * @param cwd - session working directory for relative operands. */ export function absPath(arg, cwd) { if (arg instanceof PathSpec) return arg.virtual; return resolvePath(arg, cwd); } /** * Split leading flags where some take a value (`-t STAMP`), strictly: an * unknown letter is reported instead of tolerated. * * @param args - args after the command name. * @param boolean - single-letter flags with no value. * @param valued - single-letter flags that consume the next arg. */ export function splitValueFlags(args, boolean, valued) { const flags = new Set(); const values = new Map(); const operands = []; let parsing = true; let i = 0; while (i < args.length) { const arg = args[i]; if (arg === undefined) break; const s = scopePath(arg); if (parsing && s === '--') { parsing = false; i += 1; continue; } if (parsing && s !== '-' && s.length >= 2 && s.startsWith('-') && !s.startsWith('--')) { const body = s.slice(1); for (let j = 0; j < body.length; j++) { const c = body.charAt(j); if (boolean.includes(c)) { flags.add(c); continue; } // A valued flag consumes the rest of the token (-tSTAMP) or the next // argument (-t STAMP); those trailing chars are its value, not flags, // so validation must stop here rather than pre-scanning the token. if (!valued.includes(c)) { return { flags, values, operands, bad: c }; } const rest = body.slice(j + 1); if (rest.length > 0) { values.set(c, rest); } else if (i + 1 < args.length) { i += 1; const nxt = args[i]; if (nxt !== undefined) values.set(c, wordText(nxt)); } break; } i += 1; continue; } parsing = false; operands.push(arg); i += 1; } return { flags, values, operands, bad: null }; } /** * Coerce operands to PathSpec and expand glob patterns per mount, through the * dispatcher's listings. * * A pattern spec only exists for a mounted word (classification gates it), so * the lookup propagates on a miss; a backend that lists nothing keeps the * literal spec. * * @param namespace - addressing authority (mount lookup). * @param dispatch - op dispatcher the patterns list through. * @param operands - positional operands. */ export async function expandOperands(namespace, dispatch, operands) { const out = []; for (const item of operands) { const spec = item instanceof PathSpec ? item : PathSpec.fromStrPath(item); if (spec.pattern !== null) { const mount = namespace.mountFor(spec.virtual); try { for (const item of await expandGlob(dispatch, mount, [spec], mount.vfs.walkLimit)) out.push(item); } catch (err) { if (err instanceof GlobLimitError) throw new TooManyMatches(err.message); throw err; } continue; } out.push(spec); } return out; } /** * The gated session view this builtin writes through. * * Every session write goes through the workspace's gated view, which is * what makes `preSession` rules enforceable; this used to fall back to * an ungated view over the same session, so a caller that forgot to * thread one silently wrote past every policy. A write reached without * a view is a wiring bug, not a mode, so it throws. */ export function requireView(state) { if (state === null) { throw new Error("builtin reached a session write without the workspace's gated " + 'session view; thread state from the executor arm'); } return state; } /** Render a policy denial in the builtin's own voice. */ export function refusal(cmd, err) { const encoded = encodeText(`${err.message}\n`); return [ null, new IOResult({ exitCode: 1, stderr: encoded }), new ExecutionNode({ command: cmd, exitCode: 1, stderr: encoded }), ]; } /** * The shell's own readonly refusal line, checked before the session view. * `declare`, `local` and `typeset` name themselves in it (`bash: declare: R: * readonly variable`); every other writer refuses in the assignment's voice * (`bash: R: readonly variable`). */ export function readonlyLine(cmd, name) { const voice = cmd === 'declare' || cmd === 'local' || cmd === 'typeset' ? `${cmd}: ` : ''; return `bash: ${voice}${name}: readonly variable`; } /** Render the readonly refusal (`readonlyLine`) as the result. */ export function readonlyRefusal(cmd, name) { const encoded = encodeText(`${readonlyLine(cmd, name)}\n`); return [ null, new IOResult({ exitCode: 1, stderr: encoded }), new ExecutionNode({ command: cmd, exitCode: 1, stderr: encoded }), ]; } /** A count option's value (`read -n`/`-N`, `mapfile -n`/`-s` and the rest): a run of ASCII digits, else null. */ export function countOperand(text) { return /^[0-9]+$/.test(text) ? parseInt(text, 10) : null; } /** Whether the word is a shell identifier. */ export function isValidName(name) { return IDENTIFIER_RE.test(name); } /** * Whether the word is an optionally signed run of digits, which is what * `shift`, `return` and `exit` accept as their argument. */ export function isCountWord(word) { if (!COUNT_WORD_RE.test(word)) return false; const value = BigInt(word.trim()); return value >= -(2n ** 63n) && value < 2n ** 63n; } /** A shell builtin's diagnostic in bash's voice. Mirrors Python's builtin_error. */ export function builtinError(name, message) { return encodeText(`bash: ${name}: ${message}\n`); } /** * A builtin's usage line, which bash prints after refusing one of its * options: the synopsis `help NAME` opens with. Mirrors Python's * builtin_usage. */ export function builtinUsage(name) { const synopsis = SHELL_SYNOPSES[name]; if (synopsis === undefined) throw new Error(`no synopsis: ${name}`); return `${name}: usage: ${synopsis}\n`; } /** * The words a numeric builtin reads: a leading `--` ends its options, and * bash skips it before it reads the number. Mirrors Python's numeric_operands. */ export function numericOperands(args) { return args[0] === '--' ? args.slice(1) : args; } /** A count word's value modulo 256, the status bash keeps of it. */ export function statusOf(word) { return Number(((BigInt(word.trim()) % 256n) + 256n) % 256n); } /** * The byte `read -d` and `mapfile -d` stop at. Bash takes the first byte of * the argument, not its first character (bash 5.2: `-d é` stops at 0xc3, * `-d $'\xff'` at the raw byte); an empty argument is NUL and no `-d` is a * newline. Mirrors Python's record_delimiter. */ export function recordDelimiter(text) { if (text === null) return 10; return encodeText(text)[0] ?? 0; } //# sourceMappingURL=shared.js.map