@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
302 lines • 11.6 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 { UsageError } from "../commands/errors.js";
import { textView } from "../shell/bytes.js";
import { systemNow } from "../utils/clock.js";
import { parseDateExpr } from "../utils/dates.js";
import { UTC_ZONE } from "../utils/timezone.js";
import { scanned } from "./scan.js";
export const OPERAND = '';
export const DAY = 86400;
function object(value) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError(`find: mirage-rs answered ${JSON.stringify(value)}, not an object`);
}
return value;
}
function array(value) {
if (!Array.isArray(value)) {
throw new TypeError(`find: mirage-rs answered ${JSON.stringify(value)}, not a list`);
}
return value;
}
function str(value) {
if (typeof value !== 'string') {
throw new TypeError(`find: mirage-rs answered ${JSON.stringify(value)}, not text`);
}
return value;
}
function int(value) {
if (typeof value !== 'number' || !Number.isInteger(value)) {
throw new TypeError(`find: mirage-rs answered ${JSON.stringify(value)}, not a count`);
}
return value;
}
function float(value) {
if (typeof value !== 'number') {
throw new TypeError(`find: mirage-rs answered ${JSON.stringify(value)}, not a number`);
}
return value;
}
function flag(value) {
if (typeof value !== 'boolean') {
throw new TypeError(`find: mirage-rs answered ${JSON.stringify(value)}, not a flag`);
}
return value;
}
function optInt(value) {
return value === null || value === undefined ? null : int(value);
}
function optBound(value) {
if (value === null || value === undefined)
return null;
if (typeof value !== 'string') {
throw new TypeError(`find: mirage-rs answered ${JSON.stringify(value)}, not a bound`);
}
return Number(value);
}
function words(argv) {
return argv.map((word) => [OPERAND, word]);
}
/**
* Where the expression begins in the words typed after `find`, and which
* words an `-exec` owns, read by mirage-rs without parsing the expression,
* so a line that will be refused still classifies. Mirrors Python's
* `find_layout`.
*/
export function findLayout(argv) {
const values = scanned('find_layout', words(argv));
return {
start: int(values.start),
spans: array(values.spans).map((span) => {
const [from, to] = array(span);
return [int(from), int(to)];
}),
};
}
/**
* The inclusive lower bound that means "later than `timestamp`". `-newer`
* and `-newermt` are strict (GNU: modified *more recently than*), and the
* window is inclusive, so the bound is the next representable float: exact,
* where adding an epsilon would either miss a timestamp or admit the
* reference itself.
*/
export function strictlyAfter(timestamp) {
const view = new DataView(new ArrayBuffer(8));
view.setFloat64(0, timestamp);
const bits = view.getBigUint64(0);
view.setBigUint64(0, timestamp >= 0 ? bits + 1n : bits - 1n);
return view.getFloat64(0);
}
/** One `-newermt` argument as a GNU date expression; naive times are UTC. */
export function parseNewermt(value) {
const ts = parseDateExpr(value, UTC_ZONE);
if (ts === null || !Number.isFinite(ts.getTime()) || value.trim() === '@') {
throw new UsageError(`find: I cannot figure out how to interpret '${value}' as a date or time`, 1);
}
return ts.getTime() / 1000;
}
/** One `-mtime` as inclusive epoch-second bounds: `cmp` is `+`, `-` or `=`. */
export function mtimeBounds(cmp, days, now) {
if (cmp === '+')
return [null, now - days * DAY];
if (cmp === '-')
return [now - days * DAY, null];
return [now - (days + 1) * DAY, now - days * DAY];
}
// Fold one time test into the expression's single window. The flat window
// cannot evaluate a time test per predicate node, so repeated ones fold by
// where they sit. In the top-level `-a` chain every test must hold, so the
// windows intersect: `-newer old -newer new` keeps only what is newer than
// both, and `-mtime +2 -mtime -1` keeps nothing, as GNU answers. Under
// `-o`, `!` or parentheses the window cannot be exact, so they widen to the
// union: the tautology `-mtime +0 -o -mtime -1` imposes no bounds instead
// of last-wins dropping everything (documented divergence from GNU: such a
// window over-matches).
function merge(window, bounds, fold) {
if (window === null)
return bounds;
const [oldLo, oldHi] = window;
const [lo, hi] = bounds;
if (fold === 'all') {
return [
oldLo === null ? lo : lo === null ? oldLo : Math.max(oldLo, lo),
oldHi === null ? hi : hi === null ? oldHi : Math.min(oldHi, hi),
];
}
return [
oldLo === null || lo === null ? null : Math.min(oldLo, lo),
oldHi === null || hi === null ? null : Math.max(oldHi, hi),
];
}
function rowKind(kind) {
if (kind === 'print' || kind === 'print0' || kind === 'ls' || kind === 'delete')
return kind;
throw new TypeError(`find: mirage-rs answered the action ${JSON.stringify(kind)}`);
}
function actionKind(kind) {
if (kind === 'exec' || kind === 'printf')
return kind;
return rowKind(kind);
}
function action(raw) {
const kind = str(raw.kind);
if (kind === 'exec') {
return {
kind: 'exec',
argv: array(raw.argv).map((w) => textView(str(w))),
batch: flag(raw.batch),
};
}
if (kind === 'printf') {
const parts = array(raw.parts).map((item) => {
const part = object(item);
if ('lit' in part)
return { lit: textView(str(part.lit)) };
if ('dir' in part)
return { dir: str(part.dir) };
return { time: str(part.time) };
});
return { kind: 'printf', format: textView(str(raw.format)), parts, stat: flag(raw.stat) };
}
return { kind: rowKind(kind) };
}
function node(raw, binding) {
const op = str(raw.op);
switch (op) {
case 'name':
return { op: 'name', pattern: textView(str(raw.pattern)), icase: flag(raw.icase) };
case 'path':
return { op: 'path', pattern: textView(str(raw.pattern)) };
case 'type':
return { op: 'type', kind: str(raw.kind) };
case 'empty':
return { op: 'empty' };
case 'true':
return { op: 'true' };
case 'prune':
return { op: 'prune', pruned: [], pending: [] };
case 'not':
return { op: 'not', kid: node(object(raw.kid), binding) };
case 'and':
return { op: 'and', kids: array(raw.kids).map((kid) => node(object(kid), binding)) };
case 'or':
return { op: 'or', kids: array(raw.kids).map((kid) => node(object(kid), binding)) };
case 'action': {
const kind = actionKind(str(raw.kind));
return flag(raw.batch) ? { op: 'action', kind, batch: true } : { op: 'action', kind };
}
default:
return timeNode(op, raw, binding);
}
}
function timeNode(op, raw, binding) {
const fold = str(raw.fold);
let bounds;
if (op === 'mtime') {
bounds = mtimeBounds(str(raw.cmp), float(raw.days), binding.now);
}
else if (op === 'newermt') {
bounds = [strictlyAfter(parseNewermt(textView(str(raw.date)))), null];
}
else {
const ref = binding.refs[int(raw.index)];
if (ref === undefined)
return { op: 'true' };
bounds = [strictlyAfter(ref), null];
}
binding.window = merge(binding.window, bounds, fold);
return { op: 'mtime', lo: bounds[0], hi: bounds[1] };
}
/**
* What mirage-rs read of a find line, before any time test is resolved.
* Mirrors Python's `FindParse`.
*/
export class FindParse {
values;
constructor(values) {
this.values = values;
}
/** Where the expression begins among the words typed. */
get start() {
return int(this.values.start);
}
/** The `-newer` references, as typed, in expression order. */
get newer() {
return array(this.values.newer).map((ref) => textView(str(ref)));
}
/**
* The first `-newermt` text that is no date, in expression order, with
* how many `-newer` references precede it; null when every date reads.
* GNU reads `-newer` and `-newermt` left to right, so the references
* typed before that date are statted first and their error, if any, is
* the one reported.
*/
dateRefusal() {
for (const entry of array(this.values.dates)) {
const [date, before] = array(entry);
try {
parseNewermt(textView(str(date)));
}
catch (err) {
if (err instanceof UsageError)
return [int(before), err];
throw err;
}
}
return null;
}
/**
* The expression with its time tests resolved. `newerTimes` is each
* `-newer` reference's mtime in epoch seconds, in order; empty leaves
* every `-newer` a test that always holds, as one the executor could not
* resolve. `now` is the clock `-mtime` counts from, the current time by
* default. Throws UsageError for a `-newermt` text that is no date.
*/
bind(newerTimes = [], now = systemNow()) {
const binding = { refs: newerTimes, now, window: null };
const tree = node(object(this.values.tree), binding);
const [mtimeMin, mtimeMax] = binding.window ?? [null, null];
return {
tree,
maxDepth: optInt(this.values.maxdepth),
minDepth: optInt(this.values.mindepth),
minSize: optBound(this.values.min_size),
maxSize: optBound(this.values.max_size),
mtimeMin,
mtimeMax,
usesEmpty: flag(this.values.uses_empty),
actions: array(this.values.actions).map((a) => action(object(a))),
newer: this.newer,
depthFirst: flag(this.values.depth_first),
predicates: array(this.values.predicates).map((w) => textView(str(w))),
warnings: array(this.values.warnings).map((w) => textView(str(w))),
start: this.start,
};
}
}
/**
* find's words read once by mirage-rs: the start points end where the
* expression begins, and the expression becomes a tree. Throws UsageError
* for the first word the grammar refuses, in GNU's words and with its exit
* status. Mirrors Python's `parse_find`.
*/
export function parseFind(argv) {
return new FindParse(scanned('find', words(argv)));
}
/** `parseFind` and `bind` with no reference resolved, for a caller holding only the words. */
export function parseFindExpression(argv) {
return parseFind(argv).bind();
}
//# sourceMappingURL=find.js.map