@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
233 lines • 10.5 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 { encodeText } from "../../../../shell/bytes.js";
import { SHOPT_DEFAULTS } from "../../../../shell/constants.js";
import { aliasRewrite, getText } from "../../../../shell/helpers.js";
import { singleQuote } from "../../../../utils/quote.js";
import { compareCodePoints } from "../../../../utils/sort.js";
import { ownRecord, sessionEntry, setSessionEntry, } from "../../../session/session.js";
import { scanOptions } from "../getopt.js";
import { builtinUsage, fail, finish, ok, result } from "../shared.js";
import { BAD_NAME_RE } from "./constants.js";
/** Define or print aliases. `mark` is the read the definition sits in:
* bash expands aliases as it reads, so the commands of that read still see
* the value from before it. */
export function handleAlias(args, session, mark) {
const scan = scanOptions(args, 'p');
if (scan.bad !== null)
return fail('alias', `bash: alias: ${scan.bad}: invalid option\n${builtinUsage('alias')}`, 2);
const operands = scan.operands;
const lines = [];
const errors = [];
if (operands.length === 0 || scan.letters.includes('p')) {
for (const name of Object.keys(session.aliases).sort(compareCodePoints)) {
lines.push(`alias ${name}=${singleQuote(session.aliases[name] ?? '')}`);
}
}
for (const word of operands) {
const eq = word.indexOf('=');
if (eq >= 0) {
const name = word.slice(0, eq);
if (name === '' || BAD_NAME_RE.test(name)) {
errors.push(name === ''
? `bash: alias: ${word}: not found`
: `bash: alias: \`${name}': invalid alias name`);
continue;
}
changing(session, name, mark);
setSessionEntry(session.aliases, name, word.slice(eq + 1));
continue;
}
if (BAD_NAME_RE.test(word)) {
errors.push(`bash: alias: \`${word}': invalid alias name`);
continue;
}
const val = sessionEntry(session.aliases, word);
if (val !== undefined)
lines.push(`alias ${word}=${singleQuote(val)}`);
else
errors.push(`bash: alias: ${word}: not found`);
}
return result('alias', {
out: lines.length > 0 ? encodeText(lines.map((line) => `${line}\n`).join('')) : null,
exitCode: errors.length > 0 ? 1 : 0,
stderr: errors.map((error) => `${error}\n`).join(''),
});
}
/** Remove aliases: the named ones, or all under `-a`. The commands of the
* read at `mark` still see the aliases it removes. */
export function handleUnalias(args, session, mark) {
const scan = scanOptions(args, 'a');
if (scan.bad !== null)
return fail('unalias', `bash: unalias: ${scan.bad}: invalid option\n${builtinUsage('unalias')}`, 2);
const operands = scan.operands;
if (scan.letters.includes('a')) {
for (const name of Object.keys(session.aliases))
changing(session, name, mark);
session.aliases = ownRecord();
return ok('unalias');
}
if (operands.length === 0)
return fail('unalias', builtinUsage('unalias'), 2);
const errors = [];
for (const name of operands) {
if (sessionEntry(session.aliases, name) !== undefined) {
changing(session, name, mark);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete session.aliases[name];
}
else
errors.push(`bash: unalias: ${name}: not found\n`);
}
return finish('unalias', errors);
}
function aliasesOn(session) {
return session.shopts.expand_aliases ?? SHOPT_DEFAULTS.get('expand_aliases') ?? false;
}
/** Keep the value `name` had as the read at `mark` began, the first time
* that read changes it, for the commands it read (`readValue`). Every read
* keeps its own, so a nested one (`eval`) leaves the outer read's alone. */
function changing(session, name, mark) {
let began = session.aliasMarks.get(mark.join());
if (began === undefined)
session.aliasMarks.set(mark.join(), (began = new Map()));
if (!began.has(name))
began.set(name, sessionEntry(session.aliases, name) ?? null);
}
function readValue(session, name, mark) {
const began = session.aliasMarks.get(mark.join());
if (began?.has(name) === true)
return began.get(name) ?? null;
return sessionEntry(session.aliases, name) ?? null;
}
function expanding(session, mark) {
return session.expandAliasesMarks.get(mark.join()) ?? aliasesOn(session);
}
/**
* Keep `expand_aliases` as the read at `mark` found it, before a `shopt` in
* that read changes it: the commands of that read were expanded, or not,
* already. Mirrors Python's note_expanding.
*/
export function noteExpanding(session, mark) {
if (!session.expandAliasesMarks.has(mark.join()))
session.expandAliasesMarks.set(mark.join(), expanding(session, mark));
}
/** The aliases in progress at each code unit of `node`: its slice of the
* rewritten tree holding it, or the guards another tree inherits. */
function guards(session, node) {
const scope = session.aliasExpansion;
const length = getText(node).length;
if (scope === null)
return Array(length).fill(new Set());
let root = node;
while (root.parent != null)
root = root.parent;
if (root.id === scope.root)
return scope.owners.slice(node.startIndex ?? 0, node.endIndex);
return Array(length).fill(scope.names);
}
/**
* The alias names a command word would expand as right now. bash checks a
* word where a command starts for an alias before it checks for a reserved
* word, so one of these names is a command there even when it is spelled
* `fi` or `do`. Mirrors Python's expanding_aliases.
*/
export function expandingAliases(session) {
const blocked = session.aliasExpansion?.names;
const view = session.aliasView;
const names = view !== null ? Object.keys(view) : aliasesOn(session) ? Object.keys(session.aliases) : [];
return new Set(names.filter((name) => blocked?.has(name) !== true));
}
/**
* The alias names a stored function's body is read with
* (`bodyAliasValues`). Mirrors Python's body_aliases.
*/
export function bodyAliases(session, name) {
return new Set(bodyAliasValues(session, name).keys());
}
/**
* The aliases a stored function's body is read with, by name: the ones its
* definition saw, or the session's own for a definition that kept none (one
* restored from a stored session). Mirrors Python's body_alias_values.
*/
export function bodyAliasValues(session, name) {
const site = session.functionSites.get(name);
if (site !== undefined && site.source === session.functions[name] && site.aliases != null)
return new Map(Object.entries(site.aliases));
return new Map(aliasesOn(session) ? Object.entries(session.aliases) : []);
}
/**
* The aliases a function defined by `node` keeps for its body. bash expands
* a function's aliases as it reads the definition, so the body runs them as
* they were then, whatever is defined or removed later: the aliases a use in
* the read at `mark` would expand, none while `expand_aliases` is off, and
* inside another function's body that body's own, less any alias being
* expanded where the definition stands. What is read later (`eval`,
* `source`, a trap action, `$( )`) reads the aliases as they are then.
* Mirrors Python's alias_view.
*/
export function aliasView(session, node, mark) {
const names = new Set(Object.keys(session.aliasView ?? session.aliases));
if (session.aliasView === null)
for (const name of session.aliasMarks.get(mark.join())?.keys() ?? [])
names.add(name);
const blocked = guards(session, node)[0] ?? new Set();
const view = ownRecord();
for (const name of names) {
const value = aliasValue(session, name, mark, blocked);
if (value !== null)
setSessionEntry(view, name, value);
}
return view;
}
/**
* The alias text a command word expands to, or null. bash expands aliases
* as it reads a command, so the word sees the aliases, and
* `expand_aliases`, as the read at `mark` found them. Null when they are
* not being expanded, when the word is no alias, or when it is guarded. In
* a function's body the aliases are the ones its definition saw
* (`aliasView`). Mirrors Python's alias_value.
*/
export function aliasValue(session, name, mark, blocked) {
if (blocked.has(name))
return null;
const view = session.aliasView;
if (view !== null)
return sessionEntry(view, name) ?? null;
return expanding(session, mark) ? readValue(session, name, mark) : null;
}
/**
* Replace alias words, preserving the rest of the command and its guards.
* Each insertion inherits the replaced word's guards and adds its name.
* A trailing blank checks the next caller word through the same loop.
*/
export function aliasCommandText(session, node, head, mark) {
return aliasRewrite(node, head, (name, blocked) => aliasValue(session, name, mark, blocked), guards(session, node));
}
/** The read a command at `row` of the running parse belongs to (`readRow`).
* Mirrors Python's alias_mark. */
export function aliasMark(session, row) {
return [session.parseCurrent, session.parseRow + row];
}
/** The `alias` arm; the row marks the read the definition sits in. */
export function aliasBuiltin(call) {
const session = call.context.session;
return Promise.resolve(handleAlias([...call.argv.args], session, aliasMark(session, call.row)));
}
/** The `unalias` arm. */
export function unaliasBuiltin(call) {
const session = call.context.session;
return Promise.resolve(handleUnalias([...call.argv.args], session, aliasMark(session, call.row)));
}
//# sourceMappingURL=alias.js.map