@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
113 lines • 5.43 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 { sessionAccess } from "../../context/session_context.js";
import { markEscapedGlobs } from "../../utils/glob_walk.js";
import { expandTilde } from "../../utils/path.js";
import { homeDir } from "../session/shell_dirs.js";
import { expandTemplate, makeInert, substitute } from "./brace.js";
import { classifyWord } from "./classify/index.js";
import { BRACE_LITERAL_TYPES, BRACE_WORD_TYPES } from "./constants.js";
import { splitFields } from "./fields.js";
import { callerSignal } from "../../context/session_context.js";
import { YieldBudget } from "../../io/yield_budget.js";
import { expandChunks } from "./node.js";
import { ifsValue } from "./variable.js";
import { unescapeUnquoted } from "../../shell/escapes.js";
// Brace-expand a concatenation or brace_expression into words. Literal
// word tokens form the brace template; every other child (expansions,
// strings, substitutions) expands first and joins as an inert atom, so
// `{a,$v}` alternates on the expanded value while `{1..$n}` stays
// literal, matching bash's brace-before-parameter ordering. Deliberate
// divergence: bash rewrites `$v{a,b}` to `$va $vb` before parameter
// expansion; here the prefix keeps its own expansion (`prea preb`),
// which is the useful reading.
//
// Quoting rides along per character: an atom keeps whatever marks its
// own expansion produced, and the template's escapes are marked before
// quote removal drops them, so `{'*',x}` stays literal while `{$p,x}`
// keeps the value live. Each word keeps its atoms' pieces, so an
// unquoted value still splits: `{a,b}$x` is `a$x b$x`.
async function expandBraceWord(node, context, executeFn, callStack, view) {
const session = context.session;
const pieces = [];
const atoms = [];
for (const child of node.children) {
if (child.isNamed !== true || BRACE_LITERAL_TYPES.has(child.type)) {
pieces.push(child.text);
}
else {
atoms.push(child);
pieces.push(makeInert(atoms.length - 1));
}
}
const words = expandTemplate(pieces.join(''));
if (words === null)
return null;
const values = [];
for (const atom of atoms) {
values.push(await expandChunks(atom, context, executeFn, callStack, view));
}
const home = homeDir(session);
// `{1..1000000}` is a million words: they yield as they are made, so a
// cancel or the line's deadline lands before the last.
const budget = new YieldBudget(callerSignal());
const out = [];
for (const w of words) {
out.push(substitute(expandTilde(unescapeUnquoted(markEscapedGlobs(w)), home), values));
await budget.run();
}
return out;
}
/**
* Expand parsed child nodes to words that still know their quoting.
*
* Each node expands to its pieces, which IFS then splits into fields, so
* an unquoted expansion anywhere in a word splits (`q$x` too) and quoted
* text never does. A glob character quoting made literal travels under its
* own mark, so `"/data/"*.txt` still globs while `'/data/*'.txt` does not
* and `'/data/*'?.txt` globs on the `?` alone; `unmarkGlobs` takes the
* marks off.
*/
export async function expandWords(parts, context, executeFn, callStack = null, view) {
const session = context.session;
const ifs = ifsValue(session, callStack);
const result = [];
for (const p of parts) {
if (BRACE_WORD_TYPES.has(p.type) && session.shellOptions.braceexpand !== false) {
const braceWords = await expandBraceWord(p, context, executeFn, callStack, view);
if (braceWords !== null) {
const budget = new YieldBudget(callerSignal());
for (const chunks of braceWords) {
for (const w of await splitFields(chunks, ifs))
result.push(w);
await budget.run();
}
continue;
}
}
const chunks = await expandChunks(p, context, executeFn, callStack, view);
for (const w of await splitFields(chunks, ifs))
result.push(w);
}
return result;
}
export async function expandAndClassify(words, context, executeFn, registry, cwd, callStack = null, view) {
// Words keep their glob marks, because the loop list is glob-resolved
// next (`resolveGlobs`, which is where the marks come off): `for f in
// '/data/*.txt'` iterates once over the name as typed, like bash,
// while `for f in '/data/*'?.txt` still globs on the `?`.
const expanded = await expandWords(words, context, executeFn, callStack, view);
return expanded.map((w) => classifyWord(w, registry, cwd, sessionAccess(context.session)));
}
//# sourceMappingURL=parts.js.map