UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

233 lines • 10.5 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 { 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