@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
203 lines • 6.97 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. =========
/** Build a dense array from consecutive values, starting at index 0. */
export function makeArray(values) {
return [...values];
}
/**
* One past the highest assigned index, which is what bash resolves a
* negative subscript against.
*/
export function arrayExtent(arr) {
return arr.length;
}
/** The assigned values in index order, skipping holes. */
export function arrayValues(arr) {
return arr.filter((v) => v !== null);
}
/** The assigned indices in order, skipping holes. */
export function arrayIndices(arr) {
const out = [];
arr.forEach((v, i) => {
if (v !== null)
out.push(i);
});
return out;
}
/** The number of assigned elements, which is `${#arr[@]}`. */
export function arrayCount(arr) {
return arrayValues(arr).length;
}
/** Whether `idx` holds an assigned element. */
export function arrayHas(arr, idx) {
return idx >= 0 && idx < arr.length && arr[idx] !== null && arr[idx] !== undefined;
}
/**
* The element at `idx`, or the empty string for a hole or an
* out-of-range index.
*/
export function arrayGet(arr, idx) {
if (idx < 0 || idx >= arr.length)
return '';
return arr[idx] ?? '';
}
/**
* Assign `value` at `idx`, extending with holes as needed.
*
* The subscript comes from script text, so the write goes through
* `splice` rather than `arr[idx] = value`: an element assignment on a
* caller-supplied key is a prototype-pollution shape, and `splice`
* cannot name a property at all.
*/
export function arraySet(arr, idx, value) {
while (arr.length <= idx)
arr.push(null);
arr.splice(idx, 1, value);
}
/**
* A copy of `arr` with `value` assigned at `idx`.
*
* What a writer hands the session view: the session view speaks in whole
* variables, so an element write states itself as the array the write
* produces. Building it on a copy is what keeps a refusal from leaving
* the element applied.
*/
export function arrayWith(arr, idx, value) {
const updated = [...arr];
arraySet(updated, idx, value);
return updated;
}
/**
* Take the assigned elements from index `offset` on, in index order.
*
* bash slices an indexed array by *subscript*, not by position among the
* assigned values: for `a=([1]=b [3]=d [9]=j)`, `${a[@]:2}` is `d j`
* because it keeps every index >= 2. `length` then caps how many of those
* are taken. A negative offset counts back from the extent and yields
* nothing if it is still negative.
*/
export function arraySlice(arr, offset, length) {
let start = offset;
if (start < 0) {
start += arrayExtent(arr);
if (start < 0)
return [];
}
const picked = [];
arr.forEach((v, i) => {
if (v !== null && i >= start)
picked.push(v);
});
if (length === null)
return picked;
if (length < 0)
return picked.slice(0, Math.max(0, picked.length + length));
return picked.slice(0, length);
}
/**
* Clear one element, keeping the indices of the elements after it.
*
* Trailing holes are dropped so the extent stays at one past the highest
* assigned index, matching how bash resolves `arr[-1]`.
*/
export function arrayUnset(arr, idx) {
if (idx < 0 || idx >= arr.length)
return;
arr.splice(idx, 1, null);
while (arr.length > 0 && arr[arr.length - 1] === null)
arr.pop();
}
/**
* Split one `[key]=value` literal element, null for a plain word.
*
* The split lands on the first `]=`, which is where bash finds it after
* quote removal; a key that itself holds `]=` needed quoting in bash too
* and is the one spelling this cannot recover.
*/
export function keyedWord(word) {
if (!word.startsWith('['))
return null;
const pos = word.indexOf(']=', 1);
if (pos <= 1)
return null;
return [word.slice(1, pos), word.slice(pos + 2)];
}
/**
* The indexed array a compound literal produces.
*
* A `[i]=v` element places at `i` and moves the cursor past it, a plain
* word continues from the cursor, and a repeated index keeps the last
* value, which is GNU's `([3]=x y [1]=z)` giving
* `([1]="z" [3]="x" [4]="y")`. `+=` starts the cursor at the extent
* instead of replacing. `indexOf` is async because a subscript may
* assign, and the assignment lands through the session view.
*/
export async function buildIndexedLiteral(base, words, append, indexOf) {
const arr = append && base !== null ? [...base] : [];
let cursor = append ? arrayExtent(arr) : 0;
for (const word of words) {
const keyed = keyedWord(word);
if (keyed !== null) {
let idx = await indexOf(keyed[0]);
if (idx < 0)
idx += arrayExtent(arr);
if (idx < 0)
continue;
arraySet(arr, idx, keyed[1]);
cursor = idx + 1;
}
else {
arraySet(arr, cursor, word);
cursor++;
}
}
return arr;
}
/**
* The associative array a compound literal produces.
*
* The first word picks the grammar, as GNU does: a `[key]=value` first
* word makes every plain word an error (reported back for the caller to
* render in bash's must-use-subscript voice), while a plain first word
* reads the whole list as alternating keys and values, `[a]=1`
* spellings included, literally. An odd pair list stores the last key
* with an empty value. A repeated key keeps the last value; `+=` merges
* over the existing map instead of replacing.
*/
export function buildAssocLiteral(base, words, append) {
const map = append && base !== null ? { ...base } : {};
const first = words[0];
if (first === undefined)
return { map, badWords: [] };
if (keyedWord(first) === null) {
for (let i = 0; i < words.length; i += 2) {
const key = words[i];
if (key === undefined)
break;
map[key] = words[i + 1] ?? '';
}
return { map, badWords: [] };
}
const badWords = [];
for (const word of words) {
const keyed = keyedWord(word);
if (keyed === null) {
badWords.push(word);
continue;
}
map[keyed[0]] = keyed[1];
}
return { map, badWords };
}
//# sourceMappingURL=array.js.map