@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
137 lines • 5.28 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 { callerSignal } from "../../context/session_context.js";
import { YieldBudget } from "../../io/yield_budget.js";
import { joinBytes } from "../../shell/bytes.js";
import { IFS_DEFAULT } from "../../shell/constants.js";
import { markGlobs, unmarkGlobs } from "../../utils/glob_walk.js";
import { fieldBreak, piece } from "./types.js";
/**
* What `"$*"` joins the parameters with: the first character of IFS, a
* space when IFS is unset (null) and nothing when it is empty.
*/
export function ifsJoiner(ifs) {
return ifs === null ? ' ' : ifs.slice(0, 1);
}
/**
* An expansion's value as a piece of the word it sits in. Inside double
* quotes the value is literal: its glob characters are marked and it
* never splits. Unquoted, it splits and globs.
*/
export function valuePiece(text, quoted) {
return quoted ? piece(markGlobs(text)) : piece(text, true);
}
/**
* The elements of a splat, a field boundary between each two. A long splat
* yields between its elements, so a cancel or the line's deadline lands
* mid-list.
*/
export async function splatChunks(elements, joiner, quoted) {
const out = [];
const budget = new YieldBudget(callerSignal());
for (const [index, element] of elements.entries()) {
if (index > 0)
out.push(fieldBreak(joiner));
out.push(valuePiece(element, quoted));
await budget.run();
}
return out;
}
/**
* The text of a word where no field splitting happens. An assignment, a
* `case` word, `[[ ]]` and a here-string read the pieces as one string,
* a splat boundary as its joiner.
*/
export function joinChunks(chunks) {
let out = '';
for (const c of chunks)
out += c.kind === 'piece' ? c.text : c.joiner;
return joinBytes(out);
}
/** The literal text of a word's pieces, glob marks removed. */
export function chunksText(chunks) {
return unmarkGlobs(joinChunks(chunks));
}
/**
* Split a word's pieces into fields on IFS, as bash does.
*
* Only split pieces are split. An IFS whitespace run delimits a field
* and is dropped at either end; any other IFS character, with the
* whitespace around it, delimits exactly one field, so `a,,b` under
* `IFS=,` is three fields and `a,` is one. A splat boundary always ends
* a field. A field exists once literal or quoted text, or a non-empty
* split, opened it, so an unquoted expansion that comes back empty is
* no word at all while `""` is an empty one. `ifs` is null when unset. A
* word of many pieces, or a value that splits into many fields, yields
* between them, so a cancel or the line's deadline lands mid-split; a run
* with no separator is found in one search.
*/
export async function splitFields(chunks, ifs) {
const separators = ifs ?? IFS_DEFAULT;
let blanks = '';
for (const c of IFS_DEFAULT)
if (separators.includes(c))
blanks += c;
const fields = [];
let current = '';
let started = false;
const budget = new YieldBudget(callerSignal());
const stop = separators === '' ? null : new RegExp(`[${separators.replace(/[\\\]^-]/g, '\\$&')}]`, 'g');
for (const chunk of chunks) {
await budget.run();
if (chunk.kind === 'break') {
if (started)
fields.push(current);
current = '';
started = false;
continue;
}
const text = chunk.text;
if (!chunk.split || stop === null) {
current += text;
started = started || !chunk.split || text !== '';
continue;
}
let index = 0;
while (index < text.length) {
stop.lastIndex = index;
let end = stop.exec(text)?.index ?? text.length;
if (end > index) {
current += text.slice(index, end);
started = true;
index = end;
continue;
}
while (end < text.length && blanks.includes(text[end] ?? ''))
end++;
const hard = end < text.length && separators.includes(text[end] ?? '');
if (hard) {
end++;
while (end < text.length && blanks.includes(text[end] ?? ''))
end++;
}
if (started || hard) {
fields.push(current);
await budget.run();
}
current = '';
started = false;
index = end;
}
}
if (started)
fields.push(current);
return fields.map(joinBytes);
}
//# sourceMappingURL=fields.js.map