@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
182 lines • 7.58 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 { posixEscape } from "../../rs/regex.js";
import { decodeAnsiC } from "../../shell/escapes.js";
import { NodeType as NT } from "../../shell/types.js";
import { escapeGlob } from "../../utils/glob_walk.js";
import { expandTilde } from "../../utils/path.js";
import { homeDir } from "../session/shell_dirs.js";
import { expandNode } from "./node.js";
/** An unquoted word's text: live, each character a backslash escapes literal. */
function unquotedPieces(text) {
const out = [];
let run = 0;
let i = 0;
while (i < text.length) {
if (text[i] === '\\' && i + 1 < text.length) {
const escaped = String.fromCodePoint(text.codePointAt(i + 1) ?? 0);
if (i > run)
out.push([text.slice(run, i), false]);
out.push([escaped, true]);
i += 1 + escaped.length;
run = i;
continue;
}
i += 1;
}
if (text.length > run)
out.push([text.slice(run), false]);
return out;
}
/**
* One pattern word as bash 5.2 reads it: text under any quote is literal,
* unquoted text keeps its special characters live with a backslash escaping
* the next one, an expansion's value is live when unquoted and literal inside
* double quotes, and a leading tilde names the home directory as literal
* text. Patterns are never word-split, so `$p` holding `a b` matches the word
* `a b`. `leading` says whether a tilde prefix may start the node: it starts
* the word, and the prefix, which runs to the first unquoted slash, ends
* inside it (`~""` stays literal).
*/
async function patternPieces(tsNode, context, executeFn, callStack, view, leading) {
const ntype = tsNode.type;
if (ntype === NT.WORD || ntype === NT.EXTGLOB_PATTERN || ntype === NT.REGEX) {
const raw = tsNode.text;
if (leading && raw.startsWith('~')) {
const expanded = expandTilde(raw, homeDir(context.session));
if (expanded !== raw) {
const rest = raw.slice(1);
const home = expanded.slice(0, expanded.length - rest.length);
return [[home, true], ...unquotedPieces(rest)];
}
}
return unquotedPieces(raw);
}
if (ntype === NT.RAW_STRING)
return [[tsNode.text.slice(1, -1), true]];
if (ntype === NT.ANSI_C_STRING)
return [[decodeAnsiC(tsNode.text.slice(2, -1)), true]];
if (ntype === NT.STRING)
return [[await expandNode(tsNode, context, executeFn, callStack, view), true]];
if (ntype === NT.TRANSLATED_STRING) {
for (const child of tsNode.namedChildren) {
if (child.type === NT.STRING) {
return [[await expandNode(child, context, executeFn, callStack, view), true]];
}
}
return [];
}
if (ntype === NT.CONCATENATION) {
const pieces = [];
const children = tsNode.children;
for (let position = 0; position < children.length; position += 1) {
const child = children[position];
if (child === undefined)
continue;
// A $"..." inside a concatenation arrives as an anonymous `$` token
// followed by the string node; the `$` is the translation marker,
// not text (same rule as expandNode).
if (child.type === '$' && children[position + 1]?.type === NT.STRING)
continue;
const prefix = leading &&
pieces.length === 0 &&
(position + 1 === children.length || child.text.includes('/'));
for (const piece of await patternPieces(child, context, executeFn, callStack, view, prefix))
pieces.push(piece);
}
return pieces;
}
return [[await expandNode(tsNode, context, executeFn, callStack, view), false]];
}
/**
* Expand one pattern word into the matcher's glob dialect, as case patterns,
* the right side of `[[ == ]]` and quoted parameter-expansion operands read
* it (`patternPieces`).
*/
export async function expandPattern(tsNode, context, executeFn, callStack = null, view) {
const pieces = await patternPieces(tsNode, context, executeFn, callStack, view, true);
return pieces.map(([text, literal]) => (literal ? escapeGlob(text) : text)).join('');
}
/**
* Expand the right side of `[[ =~ ]]` into a POSIX extended regex, read as a
* pattern word (`patternPieces`).
*/
export async function expandRegex(tsNode, context, executeFn, callStack = null, view) {
return regexText(await patternPieces(tsNode, context, executeFn, callStack, view, true));
}
/**
* Pattern pieces as a POSIX extended regex. A literal character is escaped,
* except inside a bracket expression, where a backslash is a member of its
* own and every character stands for itself: bash reads `['^'a]` as `[^a]`
* and `[a'-'c]` as a range. The pieces are joined as they are, so a live
* backslash escapes only a live character after it.
*/
function regexText(pieces) {
const chars = pieces.flatMap(([text, literal]) => Array.from(text, (ch) => [ch, literal]));
const at = (index) => chars[index]?.[0] ?? '';
const out = [];
let members = -1;
const unclosed = new Set();
let i = 0;
while (i < chars.length) {
const ch = at(i);
const literal = chars[i]?.[1] === true;
if (members < 0) {
if (literal) {
out.push(posixEscape(ch));
}
else if (ch === '\\' && i + 1 < chars.length && chars[i + 1]?.[1] === false) {
out.push(ch + at(i + 1));
i += 1;
}
else {
out.push(ch);
if (ch === '[') {
if (at(i + 1) === '^') {
out.push('^');
i += 1;
}
members = i + 1;
}
}
i += 1;
continue;
}
const next = at(i + 1);
// A delimiter whose closing pair is missing is missing after any later
// opening too, so the search runs once for it.
if (ch === '[' && (next === ':' || next === '.' || next === '=') && !unclosed.has(next)) {
let close = -1;
for (let j = i + 2; j < chars.length - 1; j += 1) {
if (at(j) === next && at(j + 1) === ']') {
close = j;
break;
}
}
if (close >= 0) {
for (let j = i; j < close + 2; j += 1)
out.push(at(j));
i = close + 2;
continue;
}
unclosed.add(next);
}
out.push(ch);
if (ch === ']' && i > members)
members = -1;
i += 1;
}
return out.join('');
}
//# sourceMappingURL=pattern.js.map