@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
355 lines • 16.3 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 { findLayout } from "../../../rs/find.js";
import { recurses } from "../../../commands/builtin/generic/grep.js";
import { FILE_KEYS } from "../../../commands/builtin/generic/program.js";
import { walk } from "../../../commands/cli/walk.js";
import { SPECS } from "../../../commands/spec/index.js";
import { isBuiltinGrammar } from "../../../commands/spec/builtins.js";
import { OWN_OPTION_LOOP, REFUSED, STDIN_DASH_COMMANDS, STDIN_DASH_LEADING, } from "../../../commands/spec/constants.js";
import { WordKind } from "../../../commands/spec/types.js";
import { CommandWord } from "../../../policy/types.js";
import { FlagView } from "../../../commands/spec/flag_view.js";
import { parseCommand, parseToKwargs } from "../../../commands/spec/parser.js";
import { DeviceInput } from "../../../io/types.js";
import { PathSpec } from "../../../types.js";
import {} from "../../mount/registry.js";
import { classifyBarePath } from "../../expand/classify/index.js";
import { isAssignment } from "../../../commands/builtin/generic/awk.js";
// Commands a bare invocation points at the working directory, mapped to
// the typed spelling their synthetic operand carries. GNU find/tree/du/
// ls behave exactly as if `.` had been typed (./-prefixed output); GNU
// grep -r and bare rg print bare relative names (empty raw). Two gates:
// grep only defaults under -r/-R (and ignores stdin, GNU's rule); rg
// yields to an attached stdin, even an empty one (its readable-stdin
// rule), unless `-f -` reads it for patterns or --files lists. All pinned
// on debian:stable-slim / ripgrep 14.
export const CWD_DEFAULT_RAW = {
grep: '',
rg: '',
find: '.',
tree: '.',
du: '.',
ls: '.',
};
// The path options whose file the handler itself reads or writes through
// the dispatcher, keyed by command, valued by kwarg name: curl's -o and -D,
// jq's --rawfile and --slurpfile, rg's --ignore-file, and grep's
// --exclude-from, which the executor reads with the pattern files. Like a
// program file, such a file is no operand of the mount the line runs on, so
// it routes nothing: the line runs where its positional operands (or the
// cwd) put it, and `-o` and `-D` on two mounts, or `--slurpfile` over a
// process substitution, is not cross-mount.
export const DISPATCH_FLAG_KEYS = {
curl: ['output', 'dump_header'],
grep: ['exclude_from'],
rg: ['ignore_file'],
jq: ['rawfile', 'slurpfile'],
};
// The synthetic cwd operand for a CWD_DEFAULT_RAW command typed bare.
// Injected before routing, so mount resolution, fan-out across
// descendant mounts, and respellRaw treat it exactly like a typed
// operand; backends never see the difference.
export function defaultCwdOperand(parts, cmdName, registry, cwd, stdin) {
const spec = SPECS[cmdName];
if (spec === undefined)
return null;
// A typed `-` goes back to the parser as itself, as it does from
// parseFlags, so `rg -f -` reads as stdin rather than a file `/-`.
let argv = parts
.slice(1)
.map((p) => (typeof p === 'string' ? p : p.rawPath === '-' ? '-' : p.virtual));
if (cmdName === 'find') {
// Only the words before the expression can be start points: an
// `-exec` command word or a `-newer` reference is the parser's.
argv = argv.slice(0, findLayout(argv).start);
}
const parsed = parseCommand(spec, argv, cwd, cmdName);
if (parsed.paths().length > 0)
return null;
// --type-list reads no path, so there is no cwd to walk.
if (cmdName === 'rg' && new FlagView(parseToKwargs(parsed), spec).asBool('type_list')) {
return null;
}
if (cmdName === 'grep') {
if (!recurses(new FlagView(parseToKwargs(parsed), spec)))
return null;
}
else if (cmdName === 'rg' && stdin !== null && !(stdin instanceof DeviceInput)) {
const fl = new FlagView(parseToKwargs(parsed), spec);
// `-f -` reads the attached stdin for patterns first, and --files lists
// rather than searches, and either leaves ripgrep nothing to do with
// stdin but walk the cwd instead. A stdin that is no file, FIFO or socket
// (`< /dev/null`) is not searched either (grep_cli::is_readable_stdin,
// ripgrep 14.1.1).
if (!fl.asList('file').includes('-') && !fl.asBool('files'))
return null;
}
const operand = classifyBarePath('.', registry, cwd);
if (typeof operand === 'string')
return null;
return new PathSpec({
virtual: operand.virtual,
directory: operand.directory,
vfsPath: operand.vfsPath,
pattern: operand.pattern,
resolved: operand.resolved,
rawPath: CWD_DEFAULT_RAW[cmdName] ?? '',
});
}
/**
* The path-valued options a command tree's walk resolves. `pathFlagScopes`
* reads the built-in specs, which hold no tree, and a tree's words are
* classified as its walk reads them, its group options as text. So a tree's
* group options and its leaf's are read here, walked as the run walks them:
* `notes --file /data/secret show` hands the leaf `/data/secret`, and a rule
* protecting it has to see it. Mirrors Python's `tree_path_scopes`.
*/
export function treePathScopes(registry, name, argv, cwd, env, access) {
const tree = commandTree(registry, name, cwd, argv, access);
if (tree === null)
return [];
const result = walk(name, tree.spec, argv, cwd, env ?? null);
if (result.leaf === null)
return [];
const found = [];
for (const value of Object.values(result.groupFlags)) {
for (const item of Array.isArray(value) ? value : [value]) {
if (item instanceof PathSpec)
found.push(item);
}
}
const leaf = parseCommand(result.leaf, [...result.argv], cwd, name, env, true);
for (const value of leaf.pathFlagValues) {
found.push(new PathSpec({ virtual: value, directory: value, vfsPath: '', rawPath: value }));
}
return found;
}
export function pathFlagScopes(cmdName, argv, cwd) {
const spec = SPECS[cmdName];
if (spec === undefined)
return [];
const parsed = parseCommand(spec, argv, cwd, cmdName);
const kwargs = parseToKwargs(parsed);
const flagPaths = [...parsed.pathFlagValues];
// A program file and a dispatched option's file are read or written through the
// dispatcher, not on the line's mount. A pair's name slots are words, never
// resolved paths, so they match nothing here.
for (const key of [FILE_KEYS[cmdName], ...(DISPATCH_FLAG_KEYS[cmdName] ?? [])]) {
if (key === undefined)
continue;
const value = kwargs[key];
for (const item of Array.isArray(value) ? value : [value]) {
if (typeof item !== 'string')
continue;
const index = flagPaths.indexOf(item);
if (index >= 0)
flagPaths.splice(index, 1);
}
}
return flagPaths.map((value) => new PathSpec({
virtual: value,
directory: value,
vfsPath: '',
rawPath: value,
}));
}
/**
* The path operands a line names positionally, flag values left out.
*
* Classification turns every path-shaped word into a PathSpec, including the
* value of a path-valued flag, so the classified word list cannot tell
* `tar -xf a.tar -C /mnt` (extract INTO a mount) from `tar -cf a.tar /mnt`
* (archive a whole mount). Only the spec knows which slot a word filled, so
* this asks it and keeps the classified spec for each surviving operand,
* whose `rawPath` is what a message should name.
*/
export function positionalScopes(cmdName, argv, cwd, words) {
const spec = SPECS[cmdName];
if (spec === undefined) {
return words.filter((p) => p instanceof PathSpec);
}
const parsed = parseCommand(spec, argv, cwd, cmdName);
const byVirtual = new Map();
for (const word of words) {
if (word instanceof PathSpec)
byVirtual.set(word.virtual, word);
}
return parsed.args
.filter(([, kind]) => kind === 'path')
.map(([value]) => byVirtual.get(value) ??
new PathSpec({ virtual: value, directory: value, vfsPath: '', rawPath: value }));
}
/** The handler will answer help, version or a refusal before reading any input files. */
export function optionLoopExits(cmdName, spec, argv, cwd) {
if (spec === null || !OWN_OPTION_LOOP.has(cmdName) || !isBuiltinGrammar(cmdName, spec))
return false;
const parsed = parseCommand(spec, argv, cwd, cmdName);
const fl = new FlagView(parseToKwargs(parsed), spec);
return fl.occurrences('help', 'version', REFUSED).length > 0;
}
/**
* The classified path words that route a line. Classification makes a dispatched
* option's file a path word like any other, so a command in DISPATCH_FLAG_KEYS
* routes by its positional operands alone; every other command by all its
* path words.
*/
export function routedOperands(cmdName, argv, cwd, words, pathScopes) {
if (!(cmdName in DISPATCH_FLAG_KEYS))
return pathScopes;
return positionalScopes(cmdName, argv, cwd, words);
}
/**
* Drop the operands that name no path from a line's routing words. An awk
* `var=value` operand is an assignment awk makes when its input reaches
* it, so it routes nowhere: `awk p /data/a x=1 /data/b` runs on /data like
* the same line without it. A lone `-` is standard input to the commands in
* STDIN_DASH_COMMANDS, so it routes nowhere either, past STDIN_DASH_LEADING's
* leading operands only where it names an output (split's PREFIX).
*/
export function routableScopes(cmdName, scopes) {
const leading = STDIN_DASH_LEADING.get(cmdName) ?? scopes.length;
const routed = STDIN_DASH_COMMANDS.has(cmdName)
? scopes.filter((s, index) => s.rawPath !== '-' || index >= leading)
: scopes;
if (cmdName !== 'awk')
return routed;
return routed.filter((s) => !isAssignment(s.rawPath));
}
/** Combine positional and path-flag scopes, keeping operand order. */
export function mergeScopes(positional, flagScopes) {
const merged = [...positional];
const seen = new Set(merged.map((p) => p.virtual));
for (const scope of flagScopes) {
if (!seen.has(scope.virtual)) {
seen.add(scope.virtual);
merged.push(scope);
}
}
return merged;
}
/**
* The command tree a head word walks: an installed CLI's, else one the
* mount serving the name registered (`MountEntry.cliFor`). A tree on a mount
* runs on the mount that serves its name, the working directory's when it
* has one, else the first that does, never by operand path: its walk, not a
* spec, says which words are paths. Mirrors Python's `command_tree`.
*/
export function commandTree(registry, name, cwd, argv, access) {
const install = registry.clis.get(name);
if (install !== null)
return install.cli;
const mount = argv === undefined
? registry.commandMount(name, [], cwd, access)
: registry.invocationMount(name, [], cwd, argv, access);
return mount?.cliFor(name) ?? null;
}
/**
* The line as an admission pattern reads it, and the program it runs.
*
* For a command tree's head (an installed CLI, or a tree a mount serves)
* the spec walk names the verb path (global options before the verb
* dropped, an alias canonicalized) and hands back the leaf's own words, so
* `git -C /r push origin` reads as `git push origin` and a rule on
* `git push` catches it; a walk the tree refuses (unknown verb, bare group,
* usage error) reads the raw words, since the line fails on its own.
* Anything else is the name and the raw argv, and the program is the bare
* name. `env` is the session environment, which fills a group option
* declaring `Argument.env` as the run's walk does.
*/
export function programTokens(registry, name, argv, cwd, env, access) {
const tree = commandTree(registry, name, cwd, argv, access);
if (tree !== null) {
const result = walk(name, tree.spec, argv, cwd, env);
if (result.leaf !== null) {
const program = [name, ...result.path];
return [[...program, ...result.argv], program];
}
}
return [[name, ...argv], [name]];
}
/**
* Each word's kind as a command's grammar reads it: an option, an option's
* value or an operand. A command no spec describes reads a dashed word as an
* option and anything else as an operand. `cli` says the spec is an
* installed CLI's node, whose undeclared dashed words are the program's own
* operands. Mirrors Python's `_parsed_kinds`.
*/
function parsedKinds(spec, argv, cwd, cmdName, cli = false) {
if (spec === undefined) {
return argv.map((w) => (w.startsWith('-') && w !== '-' ? WordKind.OPTION : WordKind.OPERAND));
}
const parsed = parseCommand(spec, [...argv], cwd, cmdName, undefined, cli);
return parsed.wordRoles.map((role) => role ?? WordKind.OPERAND);
}
/**
* The classified path words a builtin's grammar reads as path operands. Read
* by position, so neither an option's value nor a text operand (grep's
* pattern) counts even where its spelling repeats a path operand's, and a
* word the classifier left as text never counts.
*/
export function operandPaths(cmdName, words, cwd) {
const spec = SPECS[cmdName];
if (spec === undefined)
return words.filter((p) => p instanceof PathSpec);
const argv = words.map((p) => (typeof p === 'string' ? p : p.virtual));
const parsed = parseCommand(spec, argv, cwd, cmdName);
return words.filter((p, i) => p instanceof PathSpec &&
parsed.wordRoles[i] === WordKind.OPERAND &&
parsed.wordKinds[i] === 'path');
}
/**
* The command line word by word, each with its kind and the path it names.
* The name comes first and the redirect targets last. A command tree's verbs
* (an installed CLI's, or a mount's) are read off the tree and its leaf's
* words off the leaf's grammar; any other command's words off its own spec.
* `env` is the session environment, which fills a group option declaring
* `Argument.env` as the run's walk does. Mirrors Python's `command_words`.
*/
export function commandWords(registry, name, argv, classified, redirects, cwd, env, access) {
const tree = commandTree(registry, name, cwd, argv, access);
const result = tree !== null ? walk(name, tree.spec, argv, cwd, env) : null;
let kinds;
if (result !== null && result.leaf !== null) {
const lead = argv.length - result.argv.length;
kinds = [
...argv
.slice(0, lead)
.map((word, i) => result.verbPositions.includes(i)
? WordKind.SUBCOMMAND
: word.startsWith('-')
? WordKind.OPTION
: WordKind.VALUE),
...parsedKinds(result.leaf, argv.slice(lead), cwd, name, true),
];
}
else {
kinds = parsedKinds(SPECS[name], argv, cwd, name);
}
const words = [new CommandWord(name, WordKind.NAME)];
let option = null;
argv.forEach((text, index) => {
const kind = kinds[index] ?? WordKind.OPERAND;
const word = classified[index];
const path = word instanceof PathSpec ? word : null;
words.push(new CommandWord(text, kind, kind === WordKind.VALUE ? option : null, path));
if (kind === WordKind.OPTION)
option = text;
});
for (const p of redirects) {
words.push(new CommandWord(p.rawPath !== '' ? p.rawPath : p.virtual, WordKind.REDIRECT, null, p));
}
return words;
}
//# sourceMappingURL=routing.js.map