@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
666 lines • 35.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 { RELAY_COMMANDS } from "../../../commands/builtin/generic/crossmount/constants.js";
import { aggregateFor } from "../../../commands/builtin/generic/crossmount/detect.js";
import { handleCrossMount, isCrossMount, } from "../../../commands/builtin/generic/crossmount/index.js";
import { prepareProgram, PROGRAM_FILE_COMMANDS, programFiles, valueRefusal, } from "../../../commands/builtin/generic/program.js";
import { identityFrom } from "../../../commands/builtin/utils/identity.js";
import { invokedEnvNames, suppliedEnvNames } from "../../../commands/cli/walk.js";
import { UsageError } from "../../../commands/errors.js";
import { isBuiltinGrammar, registeredSpec } from "../../../commands/spec/builtins.js";
import { STANDARD_AFTER_SCAN } from "../../../commands/spec/constants.js";
import { spreadOperands } from "../../../commands/spec/flag_view.js";
import { SPECS } from "../../../commands/spec/index.js";
import { standardRequest } from "../../../commands/spec/standard.js";
import { currentCaller, getAdmission, sessionAccess, } from "../../../context/session_context.js";
import { IOResult, materialize } from "../../../io/types.js";
import { parseFind } from "../../../rs/find.js";
import { encodeText } from "../../../shell/bytes.js";
import { NOT_FOUND_STATUS } from "../../../shell/constants.js";
import { Follow, PathSpec } from "../../../types.js";
import { abortable, mergeSignals } from "../../../utils/abort.js";
import { concat } from "../../../utils/bytes.js";
import { compareCodePoints } from "../../../utils/sort.js";
import { globOptions, resolveGlobs } from "../../expand/globs.js";
import { Files } from "../../files.js";
import { EXEC_WRAPPERS } from "../../lookup/constants.js";
import { Consumer, JOB_BUILTINS, lineFacts, lookup, runsAs } from "../../lookup/index.js";
import { MountCommandAmbiguous } from "../../mount/errors.js";
import { pathStat } from "../../mount/namespace/probe.js";
import { MountCommandUnsupported } from "../../mount/registry.js";
import { makeCheckUnlink, makeStorageKey } from "../../mount/storage.js";
import { sessionEntry } from "../../session/session.js";
import { sessionView } from "../../session/state.js";
import { ExecutionNode } from "../../types.js";
import { operandFollow } from "../builtins/links/links.js";
import { fanOutTraversal, runWithFanout, shouldFanOut } from "../fanout.js";
import { applyFindActions } from "../find_action_dispatch.js";
import { resolveNewerRefs } from "../find_refs.js";
import { handleDisown, handleFg, handleJobs, handleKill, handlePs, handleWait } from "../jobs.js";
import { handleCli, handleMountCli, reachesService, walkLeaf } from "./cli.js";
import { optionError, parseFlags } from "./flags.js";
import { runShellFunction } from "./functions.js";
import { CWD_DEFAULT_RAW, defaultCwdOperand, mergeScopes, operandPaths, optionLoopExits, pathFlagScopes, routableScopes, routedOperands, } from "./routing.js";
import { execNode, findStartPoints, nestedShell, runOnMount } from "./run.js";
import {} from "./types.js";
// One handler per JOB_BUILTINS member but ps, which also takes the workspace
// user; lookup already narrowed the name.
const JOB_HANDLERS = {
wait: handleWait,
fg: handleFg,
kill: handleKill,
jobs: handleJobs,
disown: handleDisown,
};
/**
* Apply find's actions once, at the command boundary. Every runner below
* this point (one mount, a fan-out over nested mounts, one native run per
* cross-mount operand) only selects rows and hands them back as
* `io.records`; the actions run here over all of them together, so
* a batched `-exec {} +` is one invocation across every start point, as
* GNU's is, and a per-match action runs in start-point order.
*/
async function finishFind(stdout, io, expr, registry, context, executeFn, vfs, statPath, dispatch, stdin, starts, signal) {
const session = context.session;
const [newStdout, actionErr, actionExit] = await applyFindActions(stdout, io.records, expr, registry, session.cwd, {
...(executeFn !== undefined ? { executeFn } : {}),
session,
vfs,
statPath,
dispatch,
identity: identityFrom(sessionView(session, registry.policies, context.frame.diagnostics)),
stdin,
starts,
...(signal !== undefined ? { signal } : {}),
});
const warned = expr.warnings.map((line) => encodeText(`${line}\n`));
if (warned.length > 0 || actionErr.length > 0) {
const existing = await materialize(io.stderr);
io.stderr = concat([...warned, existing, actionErr]);
}
if (io.exitCode === 0)
io.exitCode = actionExit;
return newStdout;
}
/**
* Each path operand with what the cross-mount decision reads of it: the
* mount that owns it, the reducer of what `cmdName` resolves to there, and
* whether a mount sits below it. Mirrors Python's `_operand_mounts`.
*/
function operandMounts(registry, cmdName, scopes) {
return scopes.map((scope) => {
const mount = registry.tryMountFor(scope.virtual);
return {
path: scope,
mount: mount?.prefix ?? null,
aggregate: mount?.resolveCommand(cmdName)?.aggregate ?? null,
nests: registry.descendantMounts(scope.virtual).length > 0,
};
});
}
/** The command's words as the line spelled them, an operand as typed.
* Mirrors Python's spelled_words. */
function spelledWords(parts) {
return parts.map((p) => (p instanceof PathSpec ? p.rawPath : p));
}
/**
* Fetch the managed names the tree a mount serves under `name` reads. The
* line's fill covers installed CLIs only: which mount's tree a head word
* reaches is known only when the command runs, after any `cd` before it on the
* line. The names are fetched ahead of the command's admission, as an
* installed CLI's are, so the gate walks the tree under the values the run
* reads; an env-filled required option would otherwise stop that walk short
* of the leaf, and a writing leaf would read as a reader. A name the
* invocation assigns (`TOKEN=x tok`, `skip`) is the invocation's, so it is
* never fetched. A failed fetch throws SecretsError. Resolves to whether a
* value was fetched, which changes how the words read. Mirrors Python's
* `fill_tree_env`.
*/
export async function fillTreeEnv(name, args, session, registry, skip = new Set()) {
if (registry.fillEnv === undefined || registry.clis.get(name) !== null)
return false;
const candidates = registry.invocationCandidates(name, [], session.cwd, args, sessionAccess(session));
if (candidates.length !== 1)
return false;
const tree = candidates[0]?.cliFor(name) ?? null;
if (tree === null || lookup(name, session, registry) !== Consumer.MOUNT)
return false;
const verbs = new Set(args.filter((w) => !w.startsWith('-')));
const supplied = suppliedEnvNames(tree.spec, [...args]);
const names = new Set([...invokedEnvNames(tree.spec, verbs)].filter((n) => !supplied.has(n) && !skip.has(n)));
const before = new Map([...names].map((n) => [n, sessionEntry(session.vars, n)]));
await registry.fillEnv(session, names);
return [...names].some((n) => sessionEntry(session.vars, n) !== before.get(n));
}
/**
* The dispatch a command's own ops go through: every op it issues is made by
* the command, its admission gate included, so a rule scoped to the command
* reaches the entries below its operands, and a CLI's own reads and every
* mount run below (one run, a fan-out or a cross-mount) included. Mirrors
* Python's made_by_command.
*/
export function madeByCommand(dispatch, admission) {
return (op, path, args, kwargs, report, caller) => dispatch(op, path, args, kwargs, report, caller ?? { ...currentCaller(), ruleGate: admission });
}
export async function handleCommand(executeNode, dispatch, registry, parts, context, stdin = null, callStack = null, jobTable = null, namespace, executeFn, handed = null, signal,
// Where a function body writes its statements as they finish.
sink, parser, admission = getAdmission()) {
const session = context.session;
if (parts.length === 0) {
return [null, new IOResult(), new ExecutionNode({ command: '', exitCode: 0 })];
}
const head = parts[0];
if (head === undefined) {
return [null, new IOResult(), new ExecutionNode({ command: '', exitCode: 0 })];
}
let cmdName = typeof head === 'string' ? head : head.virtual;
const cmdStr = parts.map((p) => (typeof p === 'string' ? p : p.virtual)).join(' ');
if (JOB_BUILTINS.has(cmdName) && jobTable !== null) {
const textParts = parts.map((p) => (typeof p === 'string' ? p : p.virtual));
if (cmdName === 'ps') {
return handlePs(jobTable, textParts, session, sessionView(session, registry.policies));
}
const handler = JOB_HANDLERS[cmdName];
if (handler !== undefined) {
return handler(jobTable, textParts, session, sessionView(session, registry.policies, context.frame.diagnostics), mergeSignals(signal, context.frame.abortSignal),
// `fg` is the one job builtin that writes before it blocks.
sink);
}
}
const funcBody = session.functions[cmdName];
if (funcBody !== undefined) {
if (parser === undefined)
throw new Error('function invocation requires a parse scope');
return runShellFunction(executeNode, cmdName, funcBody, parser, parts.slice(1), context, stdin, callStack, jobTable, handed, registry.decisions, sink, executeFn ?? null);
}
dispatch = madeByCommand(dispatch, admission);
// The command's own files keep no ledger: the dispatcher accounts for the line.
const commandFiles = (follow) => new Files(dispatch, namespace === undefined ? null : namespace.readers(() => session.visibility), null, { follow, ledger: false });
// Installed CLIs: dispatch by name, never by operand path. Sits
// below functions (a user can wrap an installed CLI, bash-style)
// and above every mount branch (a CLI consults no mount).
// A CLI that works on files rather than an API (`git`) reaches them
// through `ctx.vfs`, the session's files made by the command; the
// rest never look.
const cliInstall = registry.clis.get(cmdName);
if (cliInstall !== null) {
const cliSignal = mergeSignals(signal, context.frame.abortSignal);
// A leaf that waits on its service keeps running; the caller's abort
// releases the invocation, as it does for `wait`.
return abortable(handleCli(cliInstall, parts, session, stdin, {
...(executeFn !== undefined ? { shell: nestedShell(executeFn, session, cliSignal) } : {}),
...(cliSignal !== undefined ? { signal: cliSignal } : {}),
commandLimits: registry.commandLimits,
ids: registry.ids,
entries: registry.runtimeEntries,
vfs: commandFiles(null),
session: sessionView(session, registry.policies, context.frame.diagnostics),
...(registry.processView === undefined
? {}
: { processes: registry.processView(session) }),
...(context.frame.routing === undefined ? {} : { routing: context.frame.routing }),
settle: (runtime) => registry.settle(runtime),
}, reachesService(cliInstall.cli) ? () => registry.dropCaches(cliInstall.accessor) : null), mergeSignals(signal, context.frame.abortSignal));
}
// A wrapper execs the program it names, past every function and CLI; the
// session's allow list judges the name it typed.
const typed = cmdName;
const wrapped = runsAs(cmdName, registry) !== cmdName ? EXEC_WRAPPERS.get(cmdName) : undefined;
if (wrapped !== undefined) {
const [runs, option] = wrapped;
cmdName = runs;
parts = [runs, option, ...parts.slice(1)];
}
const follow = namespace !== undefined
? operandFollow(namespace.sessionLinks(session), dispatch, cmdName, parts, session.cwd)
: undefined;
const files = commandFiles(follow ?? null);
const runCtx = {
admission,
...(signal !== undefined ? { signal } : {}),
...(follow !== undefined ? { follow } : {}),
registry,
context,
dispatch,
...(namespace !== undefined ? { namespace } : {}),
...(executeFn !== undefined ? { executeFn } : {}),
};
// Unknown name: nobody registers it; fail like bash before any
// backend work. The admission policies (fired upstream at the
// dispatch chokepoint) stay ahead of this so
// protective refusals keep their specific messages.
if (lookup(typed, session, registry) === Consumer.UNKNOWN) {
const errBytes = encodeText(`${typed}: command not found\n`);
return [
null,
new IOResult({ exitCode: NOT_FOUND_STATUS, stderr: errBytes }),
new ExecutionNode({ command: cmdStr, exitCode: NOT_FOUND_STATUS, stderr: errBytes }),
];
}
const runTree = async (mount, cli, prepared) => {
await mount.ensureReady();
const treeSignal = mergeSignals(signal, context.frame.abortSignal);
return handleMountCli(mount, cli, parts, session, stdin, {
...(executeFn !== undefined ? { shell: nestedShell(executeFn, session, treeSignal) } : {}),
...(treeSignal !== undefined ? { signal: treeSignal } : {}),
commandLimits: registry.commandLimits,
ids: registry.ids,
vfs: files,
session: sessionView(session, registry.policies, context.frame.diagnostics),
...(registry.processView === undefined ? {} : { processes: registry.processView(session) }),
}, (path) => registry.tryMountFor(path), follow, prepared);
};
// A command tree a mount serves runs by name on that mount, like an
// installed CLI: its walk, not a spec parse, reads the words, so it sits
// ahead of the help, cross-mount and flag-parsing branches below, and of
// the operand a bare ls, du, tree, find or grep implies.
let treeMount = registry.invocationMount(cmdName, [], session.cwd, spelledWords(parts).slice(1), sessionAccess(session));
const tree = treeMount?.cliFor(cmdName) ?? null;
if (treeMount !== null && tree !== null) {
const discovered = treeMount;
const walked = await walkLeaf(cmdName, tree, parts, session, files);
if (Array.isArray(walked) && walked[1].exitCode === 0)
return walked;
try {
treeMount = registry.executionMount(cmdName, [], session.cwd, spelledWords(parts).slice(1), sessionAccess(session));
}
catch (err) {
if (!(err instanceof MountCommandAmbiguous))
throw err;
const stderr = encodeText(`${err.message}\n`);
return [
null,
new IOResult({ exitCode: 1, stderr }),
new ExecutionNode({ command: cmdStr, exitCode: 1, stderr }),
];
}
if (Array.isArray(walked))
return walked;
const selectedTree = treeMount?.cliFor(cmdName) ?? null;
if (treeMount !== null && selectedTree !== null) {
return runTree(treeMount, selectedTree, treeMount === discovered ? walked : undefined);
}
}
// A mount may register its own command under a builtin's name. The steps
// below that read the line with the builtin's grammar then skip a line
// whose operands (or cwd) pick such a mount, as resolveMount picks it, and
// the line routes by those operands alone. Only a word classified as a path
// in a path operand slot picks one: not a file an option reads (`grep -f`),
// a pattern, or a word the cwd's spec left as text.
const operands = operandPaths(cmdName, parts.slice(1), session.cwd).filter((s) => s.walkError !== 'ENOENT');
const ownMount = registry.invocationMount(cmdName, operands, session.cwd, spelledWords(parts).slice(1), sessionAccess(session));
const ownSpec = ownMount?.specFor(cmdName) ?? null;
const borrowed = SPECS[cmdName] !== undefined && ownSpec !== null && !isBuiltinGrammar(cmdName, ownSpec);
if (!borrowed && cmdName in CWD_DEFAULT_RAW) {
const operand = defaultCwdOperand(parts, cmdName, registry, session.cwd, stdin);
if (operand !== null) {
// Where GNU's implied `.` sits: after the pattern for grep/rg
// (the first positional is the pattern), right after the command
// name for find/tree/du/ls (find's expression tokens must stay
// behind the path).
parts =
cmdName === 'grep' || cmdName === 'rg'
? [...parts, operand]
: [head, operand, ...parts.slice(1)];
}
}
const pathScopes = [];
for (let i = 1; i < parts.length; i++) {
const p = parts[i];
if (p instanceof PathSpec)
pathScopes.push(p);
}
const rawArgv = parts.slice(1).map((p) => (typeof p === 'string' ? p : p.virtual));
// --help and --version answer from the package, never from a backend, so
// they are served before mount permission checks and cross-mount routing:
// otherwise `rm --version /ro/x` hits the read-only refusal,
// `cat --version /ram/a /disk/b` parses against the shared spec, which
// carries no injected --version, and fails as an unknown option, and
// `mv --help /ram/a /disk/b` reaches the cross-mount relay, which bypasses
// the registered wrapper that answers help and MOVED THE FILE instead of
// printing the page.
const cmdMount = ownMount ?? registry.mountForCommand(cmdName);
const cmdSpec = cmdMount?.specFor(cmdName) ?? null;
const standardOut = standardRequest(cmdName, cmdSpec, rawArgv);
const builtinSpec = SPECS[cmdName];
if (standardOut !== null &&
STANDARD_AFTER_SCAN.has(cmdName) &&
isBuiltinGrammar(cmdName, cmdSpec) &&
builtinSpec !== undefined) {
const refused = valueRefusal(cmdName, parseFlags(parts.slice(1), registeredSpec(cmdName, builtinSpec), cmdName, session.cwd)
.flagKwargs);
if (refused !== null) {
return [
null,
refused,
new ExecutionNode({
command: cmdStr,
exitCode: refused.exitCode,
stderr: await materialize(refused.stderr),
}),
];
}
}
if (standardOut !== null) {
return [standardOut, new IOResult(), new ExecutionNode({ command: cmdStr, exitCode: 0 })];
}
let prepared = null;
if (!borrowed && PROGRAM_FILE_COMMANDS.has(cmdName)) {
const programSpec = SPECS[cmdName];
if (programSpec !== undefined) {
const candidate = parseFlags(parts.slice(1), registeredSpec(cmdName, programSpec), cmdName, session.cwd);
if (programFiles(cmdName, candidate.flagKwargs).length > 0) {
prepared = candidate;
const refusal = optionError(cmdName, prepared);
if (refusal !== null) {
const [msg, code] = refusal;
return [
null,
new IOResult({ exitCode: code, stderr: msg }),
new ExecutionNode({ command: cmdStr, exitCode: code, stderr: msg }),
];
}
const [texts, flags, remaining, error] = await prepareProgram(cmdName, prepared.texts, prepared.flagKwargs, stdin, dispatch, prepared.paths);
if (error !== null) {
return [
null,
error,
new ExecutionNode({
command: cmdStr,
exitCode: error.exitCode,
stderr: await materialize(error.stderr),
}),
];
}
stdin = remaining;
prepared = { ...prepared, texts, flagKwargs: flags };
pathScopes.splice(0, pathScopes.length, ...prepared.paths);
}
}
}
// Path-valued flags (e.g. shuf --output=/dst/out) own a mount just like
// positional operands, so they join routing and mount validation instead of
// being dropped whenever a positional path is also present.
// The empty name joins onto the working directory in `virtual` but names
// no path there, so it routes nowhere: the line runs where its other
// operands (or the cwd) put it, and that run's op guards refuse it. A line
// is not cross-mount because one of its words is empty.
// A prepared program line already holds its positional operands.
let routingScopes = borrowed ? operands : [];
let flagScopes = [];
if (!borrowed &&
!optionLoopExits(cmdName, cmdMount?.specFor(cmdName) ?? null, rawArgv, session.cwd)) {
const routed = routableScopes(cmdName, prepared !== null
? pathScopes
: routedOperands(cmdName, rawArgv, session.cwd, parts.slice(1), pathScopes));
flagScopes = pathFlagScopes(cmdName, rawArgv, session.cwd);
routingScopes = mergeScopes(routed, flagScopes).filter((s) => s.walkError !== 'ENOENT');
}
let findExpr = null;
if (!borrowed && cmdName === 'find') {
try {
const parsedFind = parseFind(spelledWords(parts.slice(1)));
// Every -newer reference is statted through the dispatcher here,
// once, before the expression is bound; a date GNU refuses stops the
// stats at the references typed before it.
const badDate = parsedFind.dateRefusal();
const refs = badDate === null ? parsedFind.newer : parsedFind.newer.slice(0, badDate[0]);
let newerTimes = [];
if (refs.length > 0) {
let refErr;
[newerTimes, refErr] = await resolveNewerRefs(refs, registry, session.cwd, (path) => pathStat(dispatch, path), namespace ?? null, lineFacts(cmdName, parts, session.cwd).follows !== Follow.NONE);
if (refErr !== null) {
return [
null,
new IOResult({ exitCode: 1, stderr: refErr }),
new ExecutionNode({ command: cmdStr, stderr: refErr, exitCode: 1 }),
];
}
}
if (badDate !== null)
throw badDate[1];
findExpr = parsedFind.bind(newerTimes);
}
catch (err) {
if (err instanceof UsageError) {
const errBytes = encodeText(`${err.message}\n`);
return [
null,
new IOResult({ exitCode: err.exitCode, stderr: errBytes }),
new ExecutionNode({ command: cmdStr, stderr: errBytes, exitCode: err.exitCode }),
];
}
throw err;
}
}
// Path-valued flags count: `cp -t /other/mount/dir src` spans mounts
// exactly like a positional destination would.
if (!borrowed &&
isCrossMount(cmdName, operandMounts(registry, cmdName, routingScopes), flagScopes)) {
// Parse against the shared spec so flags and text operands do not
// depend on the source mount: raw argv would hand flag tokens ("-c")
// to the generic as the search pattern. The bound single-mount runner
// lets the strategy runners execute each operand natively on its
// owning mount.
// Registered, not declared: the registry injects --help/--version into
// every spec, so parsing the declaration here made the same word mean
// two things by mount count -- `cat --vers=x /ram/a` was
// `option '--version' doesn't allow an argument` and the two-mount line
// was `unrecognized option '--vers=x'`.
const sharedSpec = SPECS[cmdName] ?? cmdMount?.specFor(cmdName) ?? undefined;
const csParsed = prepared ??
parseFlags(parts.slice(1), sharedSpec !== undefined ? registeredSpec(cmdName, sharedSpec) : null, cmdName, session.cwd);
let csFlags = csParsed.flagKwargs;
const csTexts = findExpr !== null ? rawArgv.slice(findExpr.start) : csParsed.texts;
const csRefusal = optionError(cmdName, csParsed);
if (csRefusal !== null) {
const [msg, code] = csRefusal;
return [
null,
new IOResult({ exitCode: code, stderr: msg }),
new ExecutionNode({ command: cmdStr, exitCode: code, stderr: msg }),
];
}
// A path option's value (sort -o, cp -t, csplit -f) routes to its owning
// mount but is not an input. Parsed operands preserve aliases, order, and
// repeated path values. find's expression is not the spec's grammar, so
// its start points are the words classified as paths.
let csScopes = findExpr !== null ? pathScopes : csParsed.paths;
if (RELAY_COMMANDS.has(cmdName)) {
// STREAM and FANOUT (and a custom command's reducer) run each operand
// natively on its mount, which expands the operand's glob. RELAY sees every operand at once (wc's
// layout, cp's sources), so its glob operands must expand here; an
// unmatched glob stays the literal word, like bash. One operand at a
// time, so join's option loop sees each match where its glob was typed.
const groups = [];
for (const scope of csScopes) {
const expanded = await resolveGlobs([scope], registry, false, namespace === undefined ? null : namespace.sessionLinks(session), globOptions(session));
groups.push(expanded.filter((p) => typeof p !== 'string'));
}
csScopes = groups.flat();
csFlags = spreadOperands(csFlags, groups.map((group) => group.map((p) => p.rawPath)));
}
const runSingle = (name, ps, ts, fk, opts) => runOnMount(runCtx, name, ps, ts, fk, {
...(findExpr !== null ? { find: findExpr } : {}),
...opts,
});
// A per-operand native run is single-mount by construction, so a
// traversal operand holding nested mounts has to fan out inside it,
// exactly as the same operand would on a line of its own.
const csStat = (path) => pathStat(dispatch, path);
const runOperand = runWithFanout(runSingle, registry, session.cwd, files, sessionView(session, registry.policies, context.frame.diagnostics), mergeSignals(signal, context.frame.abortSignal), dispatch, follow, findExpr ?? undefined, parts);
const [csStdout0, csIo] = await handleCrossMount(cmdName, csScopes, csTexts, csFlags, dispatch, runOperand, stdin, makeStorageKey(registry), files, sessionView(session, registry.policies, context.frame.diagnostics), session.cwd, spelledWords(parts.slice(1)), aggregateFor(cmdName, operandMounts(registry, cmdName, csScopes)), makeCheckUnlink(registry, sessionAccess(session)), follow, registry.diff, registry.ids, mergeSignals(signal, context.frame.abortSignal));
const csExec = new ExecutionNode({
command: cmdStr,
stderr: await csIo.materializeStderr(),
exitCode: csIo.exitCode,
});
let csStdout = csStdout0;
if (findExpr !== null) {
csStdout = await finishFind(csStdout, csIo, findExpr, registry, context, executeFn, files, csStat, dispatch, stdin, csScopes, mergeSignals(signal, context.frame.abortSignal));
csExec.exitCode = csIo.exitCode;
csExec.stderr = await csIo.materializeStderr();
}
if (csParsed.warnings.length > 0) {
const csWarn = encodeText(csParsed.warnings.map((w) => `${cmdName}: ${w}\n`).join(''));
const csExisting = await materialize(csIo.stderr);
csIo.stderr = concat([csWarn, csExisting]);
csExec.stderr = concat([csWarn, csExec.stderr]);
}
// The native sub-runs carry their own mount's scope; the cross-mount
// command as a whole is bounded by the strictest cap across the
// operand mounts, regardless of which sub-run merged last.
const mounts = [];
for (const s of pathScopes) {
// a scope outside any mount contributes nothing here
const m = registry.tryMountFor(s.virtual);
if (m !== null)
mounts.push(m);
}
csIo.producer = {
command: cmdName,
prefixes: mounts.map((m) => m.prefix),
declared: null,
};
csExec.paths = pathScopes;
return [csStdout, csIo, csExec];
}
// Path-flag targets count: a command bound to one mount cannot write its
// output through another. A borrowed line runs on its predicted mount, so
// every path its own spec reads, operand or option value, must lie there
// too, or its handler reads another mount's path through its own.
let spanScopes = routingScopes;
const mountPrefixes = new Set();
if (borrowed && ownMount !== null) {
const own = parseFlags(parts.slice(1), ownSpec, cmdName, session.cwd);
spanScopes = [
...own.paths,
...Object.values(own.flagKwargs)
.flatMap((value) => (Array.isArray(value) ? value : [value]))
.filter((item) => item instanceof PathSpec),
];
mountPrefixes.add(ownMount.prefix);
}
for (const s of spanScopes) {
// a scope outside any mount contributes nothing here
const m = registry.tryMountFor(s.virtual);
if (m !== null)
mountPrefixes.add(m.prefix);
}
if (mountPrefixes.size > 1) {
const prefixesStr = [...mountPrefixes].sort(compareCodePoints).join(', ');
const err = encodeText(`${cmdName}: paths span multiple mounts (${prefixesStr}), cross-mount not supported\n`);
return [
null,
new IOResult({ exitCode: 1, stderr: err }),
new ExecutionNode({ command: cmdStr, exitCode: 1 }),
];
}
let mount;
try {
mount = await registry.resolveMount(cmdName, routingScopes, session.cwd, rawArgv, sessionAccess(session));
}
catch (err) {
if (err instanceof MountCommandUnsupported || err instanceof MountCommandAmbiguous) {
const errBytes = encodeText(`${err.message}\n`);
return [
null,
new IOResult({ exitCode: 1, stderr: errBytes }),
new ExecutionNode({ command: cmdStr, stderr: errBytes, exitCode: 1 }),
];
}
throw err;
}
if (mount === null) {
const err = encodeText(`${cmdName}: command not found`);
return [
null,
new IOResult({ exitCode: NOT_FOUND_STATUS, stderr: err }),
new ExecutionNode({ command: cmdStr, exitCode: NOT_FOUND_STATUS }),
];
}
const selectedTree = mount.cliFor(cmdName);
if (selectedTree !== null)
return runTree(mount, selectedTree);
const parsedLine = prepared ?? parseFlags(parts.slice(1), mount.specFor(cmdName), cmdName, session.cwd);
const { paths: parsedPaths, flagKwargs, warnings: parseWarnings } = parsedLine;
const textsRaw = parsedLine.texts;
const refusal = optionError(cmdName, parsedLine);
if (refusal !== null) {
const [msg, code] = refusal;
return [
null,
new IOResult({ exitCode: code, stderr: msg }),
new ExecutionNode({ command: cmdStr, exitCode: code, stderr: msg }),
];
}
const texts = findExpr !== null ? rawArgv.slice(findExpr.start) : textsRaw;
// The start points are the path words before the expression; the
// spec's rest slot would otherwise read an -exec word as one.
const paths = findExpr !== null
? findStartPoints(parts.slice(1), findExpr.start, mount.specFor(cmdName), session.cwd)
: parsedPaths;
if (findExpr !== null) {
// `multiple: true` on find value-flags makes parseToKwargs emit arrays;
// bespoke backend wrappers read these as scalars. Migrated backends read
// the expression from `texts` and ignore flagKwargs.
for (const [key, value] of Object.entries(flagKwargs)) {
if (Array.isArray(value)) {
const last = value.at(-1);
if (last !== undefined)
flagKwargs[key] = last;
}
}
}
const warnBytes = parseWarnings.length > 0
? encodeText(parseWarnings.map((w) => `${cmdName}: ${w}\n`).join(''))
: null;
const singleStat = (path) => pathStat(dispatch, path);
if (!borrowed && shouldFanOut(cmdName, paths, parts, session.cwd, registry)) {
const [fanOut0, fanIo, fanNode] = await fanOutTraversal(cmdName, paths, texts, flagKwargs, registry, mount, session.cwd, cmdStr, stdin, files, sessionView(session, registry.policies, context.frame.diagnostics), mergeSignals(signal, context.frame.abortSignal), dispatch, (name, ps, ts, fk, opts) => runOnMount(runCtx, name, ps, ts, fk, {
...(findExpr !== null ? { find: findExpr } : {}),
...opts,
argv: spelledWords(parts.slice(1)),
}), follow, findExpr ?? undefined);
let fanOut = fanOut0;
if (findExpr !== null) {
fanOut = await finishFind(fanOut, fanIo, findExpr, registry, context, executeFn, files, singleStat, dispatch, stdin, paths, mergeSignals(signal, context.frame.abortSignal));
fanNode.exitCode = fanIo.exitCode;
fanNode.stderr = await materialize(fanIo.stderr);
}
if (warnBytes !== null) {
const existing = await materialize(fanIo.stderr);
fanIo.stderr = concat([warnBytes, existing]);
fanNode.stderr = concat([warnBytes, fanNode.stderr]);
}
return [fanOut, fanIo, fanNode];
}
const [rawStdout, io] = await runOnMount(runCtx, cmdName, paths, texts, flagKwargs, {
stdin,
mount,
resolveHint: routingScopes[0] ?? null,
argv: spelledWords(parts.slice(1)),
...(findExpr !== null ? { find: findExpr } : {}),
});
let stdout = rawStdout;
if (findExpr !== null) {
stdout = await finishFind(stdout, io, findExpr, registry, context, executeFn, files, singleStat, dispatch, stdin, paths, mergeSignals(signal, context.frame.abortSignal));
}
if (warnBytes !== null) {
const existing = await materialize(io.stderr);
io.stderr = concat([warnBytes, existing]);
}
return [stdout, io, await execNode(cmdStr, io, paths)];
}
//# sourceMappingURL=command.js.map