@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
125 lines • 5.95 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 { storedFunctionText } from "../../../../shell/printer.js";
import { BASH_BUILTINS, KEYWORDS } from "../../../lookup/constants.js";
import { lookup, lookupAll, program } from "../../../lookup/lookup.js";
import { Consumer } from "../../../lookup/types.js";
import { BIN_PREFIX } from "../../../../shell/constants.js";
import { DESCRIPTIONS } from "./constants.js";
import { NameKind } from "./types.js";
import { sessionEntry } from "../../../session/session.js";
import { bodyAliasValues } from "../alias/alias.js";
// The kind one layer reports a name as. A function is a function and one
// of bash's own builtins is a builtin; every other layer runs a program,
// whose file is the name's under /usr/bin.
function kind(consumer, name) {
if (consumer === Consumer.FUNCTION)
return NameKind.FUNCTION;
if (consumer === Consumer.SESSION && BASH_BUILTINS.has(name))
return NameKind.BUILTIN;
return NameKind.FILE;
}
/**
* Classify the name as the layer that would run it, null if none does.
*
* A layer that would run a program reports one only where the name has a
* file (`program`): a word only the external fallback capture takes still
* runs, but is not found, as bash reports a name its
* `command_not_found_handle` would take.
*/
export function classify(name, session, registry) {
if (sessionEntry(session.aliases, name) !== undefined)
return NameKind.ALIAS;
if (KEYWORDS.has(name))
return NameKind.KEYWORD;
const consumer = lookup(name, session, registry);
if (consumer === Consumer.UNKNOWN)
return null;
const nameKind = kind(consumer, name);
if (nameKind === NameKind.FILE && program(name, session, registry) === null)
return null;
return nameKind;
}
/**
* Classify every layer holding the name, most-preferred first.
*
* A reserved word goes first and does not end the walk: bash prints both
* lines when a function shares a keyword's name (pinned:
* `function time { :; }; type -a time` prints the keyword line then the
* function line). mirage's parser is looser than bash's about reserved
* words as function names, so the shadow is reachable here for any of
* them, and hiding it would leave `type -a` claiming a keyword while the
* line runs the function.
*
* Duplicate kinds are dropped, since the kinds are coarser than the
* layers: a program both a mount and a CLI answer for is one file. A
* builtin that is a program too ends with that file's line, as bash's
* `type -a echo` does after its builtin line. A file is reported only
* where `program` finds one, so a layer the name runs from without a file
* (the external fallback, a mount command under a shell-only builtin)
* prints no line.
*/
export function classifyAll(name, session, registry) {
// An alias is reported first and whether or not `expand_aliases` is
// on, as bash does: `type` describes the definition, not whether the
// parser is currently applying it.
const kinds = sessionEntry(session.aliases, name) !== undefined ? [NameKind.ALIAS] : [];
if (KEYWORDS.has(name))
kinds.push(NameKind.KEYWORD);
const hasFile = program(name, session, registry) !== null;
for (const consumer of lookupAll(name, session, registry)) {
const nameKind = kind(consumer, name);
if (nameKind === NameKind.FILE && !hasFile)
continue;
if (!kinds.includes(nameKind))
kinds.push(nameKind);
}
if (!kinds.includes(NameKind.FILE) && hasFile)
kinds.push(NameKind.FILE);
return kinds;
}
/**
* The kinds to report for one name: hide a layer, then take the top.
*
* Hiding is a filter over the layer list, never an edit to the session,
* and it runs before the winner is picked. That order is what keeps the
* winner honest: `type -f` reports the layer under a shadowing function,
* where filtering afterwards would report nothing at all.
*/
export function locations(name, session, registry, allMode, drop = null) {
let kinds = classifyAll(name, session, registry);
if (drop !== null)
kinds = kinds.filter((kind) => kind !== drop);
return allMode ? kinds : kinds.slice(0, 1);
}
/** The path of a program's file, where PATH finds it. */
export function programFile(name) {
return `${BIN_PREFIX}/${name}`;
}
/** Render the verbose line `command -V` and `type` print. A function's line
* is followed by its body as `declare -f` prints it. `session` is needed to
* read an alias's value and a function's body; every other kind renders from
* the name alone. */
export function describe(name, kind, session, parser) {
if (kind === NameKind.ALIAS && session !== undefined) {
return `${name} is aliased to \`${sessionEntry(session.aliases, name) ?? ''}'`;
}
const body = session === undefined ? undefined : sessionEntry(session.functions, name);
if (kind === NameKind.FUNCTION && session !== undefined && body !== undefined)
return `${name} is a function\n${storedFunctionText(name, body, parser, bodyAliasValues(session, name))}`;
if (kind === NameKind.FILE)
return `${name} is ${programFile(name)}`;
return `${name} is ${DESCRIPTIONS[kind] ?? ''}`;
}
//# sourceMappingURL=classify.js.map