@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
289 lines • 11.2 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 { 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