@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
125 lines • 5.38 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 { boundedDecimal } from "../../../../commands/builtin/utils/strtol.js";
import { decodeText, encodeText } from "../../../../shell/bytes.js";
import { builtinError, builtinUsage, result } from "../shared.js";
import { SIGNALS } from "../timeout/constants.js";
import { PSEUDO_SIGNALS, RUN_EVENTS, SIGNAL_MAX } from "./constants.js";
import { TrapEvent } from "./types.js";
/**
* What a signal spec names: EXIT, ERR or RETURN, another signal bash knows
* (which mirage cannot deliver), or nothing bash would accept. A name may
* carry `SIG` and any case; a number is one too.
*/
export function eventOf(spec) {
if (/^[0-9]+$/.test(spec)) {
const number = boundedDecimal(spec, BigInt(SIGNAL_MAX) + 1n);
if (number === null)
return null;
return number === 0n ? TrapEvent.Exit : TrapEvent.Other;
}
const name = spec.toUpperCase();
if (Object.hasOwn(RUN_EVENTS, name))
return name;
if (PSEUDO_SIGNALS.has(name))
return TrapEvent.Other;
const base = name.startsWith('SIG') ? name.slice(3) : name;
if ([...SIGNALS.values()].includes(base))
return TrapEvent.Other;
if (base.startsWith('RTMIN') || base.startsWith('RTMAX'))
return TrapEvent.Other;
return null;
}
/** One `trap -p` row, the action single-quoted the way bash does. */
export function listing(action, event) {
return `trap -- '${action.replaceAll("'", "'\\''")}' ${event}\n`;
}
/**
* Register, reset or list the shell's `EXIT`, `ERR` and `RETURN` actions.
*
* EXIT runs where the shell ends: at `exit` (in the frame that called it),
* at the end of a child shell, or when an error ends the shell. A line of a
* persistent session is not the end of its shell, so it runs nothing there.
* ERR runs after a command fails where `set -e` would act, RETURN after a
* function or a sourced file returns (`executor/traps.ts`). Mirage delivers
* no signals, so any other event bash knows is refused rather than accepted
* and never run.
*/
export function handleTrap(args, session) {
const words = [...args];
let printing = false;
for (let word = words[0]; word?.startsWith('-') === true && word !== '-'; word = words[0]) {
words.shift();
if (word === '--')
break;
for (const flag of word.slice(1)) {
if (flag === 'p')
printing = true;
else if (flag === 'l') {
return result('trap', { exitCode: 2, stderr: 'mirage: trap: -l: not supported\n' });
}
else {
const message = decodeText(builtinError('trap', `-${flag}: invalid option`));
return result('trap', { exitCode: 2, stderr: message + builtinUsage('trap') });
}
}
}
const errors = [];
if (printing || words.length === 0) {
const out = [];
for (const spec of words.length > 0 ? words : Object.keys(RUN_EVENTS)) {
const event = eventOf(spec);
if (event === null) {
errors.push(decodeText(builtinError('trap', `${spec}: invalid signal specification`)));
}
else if (event !== TrapEvent.Other) {
const action = session[RUN_EVENTS[event][0]];
if (action !== null)
out.push(listing(action, event));
}
}
const text = out.join('');
return result('trap', {
out: text !== '' ? encodeText(text) : null,
exitCode: errors.length > 0 ? 1 : 0,
stderr: errors.join(''),
});
}
const [first = '', ...rest] = words;
if (rest.length === 0 && eventOf(first) === null) {
return result('trap', { exitCode: 2, stderr: builtinUsage('trap') });
}
const action = rest.length === 0 ? '-' : first;
const specs = rest.length === 0 ? words : rest;
for (const spec of specs) {
const event = eventOf(spec);
if (event === null) {
errors.push(decodeText(builtinError('trap', `${spec}: invalid signal specification`)));
}
else if (event !== TrapEvent.Other) {
const [field, scoped] = RUN_EVENTS[event];
session[field] = action === '-' ? null : action;
session[scoped] = false;
}
else if (action !== '-') {
errors.push(`mirage: trap: ${spec}: not supported\n`);
}
}
return result('trap', { exitCode: errors.length > 0 ? 1 : 0, stderr: errors.join('') });
}
/** The `trap` arm. */
export function trapBuiltin(call) {
return Promise.resolve(handleTrap(call.argv.args, call.context.session));
}
//# sourceMappingURL=trap.js.map