UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

452 lines • 18.3 kB
// ========= 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 { findChild, listedNode, subcommandRows } from "../../../../commands/cli/walk.js"; import { BUILTIN_SPECS } from "../../../../commands/spec/builtins.js"; import { argparseParts, flagRows, operandRows, renderHelp, usageLine, } from "../../../../commands/spec/help.js"; import { parseShellOptions, SHELL_SPECS } from "../../../../commands/spec/shell.js"; import { SHELL_SYNOPSES, SYNOPSES } from "../../../../commands/spec/synopsis.js"; import { UsageStyle } from "../../../../commands/spec/types.js"; import { RegexError } from "../../../../rs/errors.js"; import { posixRegex } from "../../../../rs/regex.js"; import { byteView, encodeText, utf8Locale } from "../../../../shell/bytes.js"; import { ShellBuiltin } from "../../../../shell/types.js"; import { compareCodePoints } from "../../../../utils/sort.js"; import { VERSION } from "../../../../version.js"; import { BASH_BUILTINS, EXEC_WRAPPERS } from "../../../lookup/constants.js"; import { DEV_PREFIX } from "../../../mount/registry.js"; import { envSnapshot } from "../../../session/state.js"; import { envWidth, fill, optionRows, paragraphs, summary } from "../help/help.js"; import { result } from "../shared.js"; import { DEFAULT_WIDTH, HINT, MANUAL, MAX_WIDTH, MIN_WIDTH, NO_PAGE, SECTION, SECTION_RE, TAG_INDENT, TEXT_INDENT, WHATIS_WIDTH, } from "./constants.js"; const SHELL_WORDS = new Set(Object.values(ShellBuiltin)); // Builtins whose page is the spec of another name. const SPEC_OF = new Map([['sh', 'bash']]); /** A page's sections after NAME, leaving out the empty ones. */ function sections(synopsis, description, options, extra = []) { const out = [['SYNOPSIS', [synopsis]]]; if (description.length > 0) out.push(['DESCRIPTION', description]); if (options.length > 0) out.push(['OPTIONS', options]); for (const [head, blocks] of extra) if (blocks.length > 0) out.push([head, blocks]); return out; } /** * A spec's DESCRIPTION: what its description says after the summary NAME * carries, and its epilog; the summary alone when that is all. */ function described(spec) { const [lead = '', ...text] = paragraphs(spec.description); const rest = [...text, ...paragraphs(spec.epilog)]; if (rest.length > 0) return rest; return lead !== '' ? [lead] : []; } /** The page of a shell builtin, from the spec `help` shows it by: its epilog is the exit status. */ export function builtinPage(name) { const spec = SHELL_SPECS[name]; if (spec === undefined) throw new Error(`no shell spec: ${name}`); const [lead = '', ...text] = paragraphs(spec.description); const status = spec.epilog !== null && spec.epilog !== '' ? [['EXIT STATUS', [spec.epilog]]] : []; return { name, summary: lead, sections: sections(SHELL_SYNOPSES[name] ?? '', text.length > 0 ? text : [lead], optionRows(spec), status), }; } /** * The page of a command, from its spec. The synopsis is the program's own * first `--help` line where mirage keeps one, else the line the spec's slots * spell. */ export function specPage(name, spec) { const synopsis = SYNOPSES[name] ?? usageLine(name, spec, [], spec.usageStyle).replace(/^Usage: /, ''); return { name, summary: summary(spec), sections: sections(synopsis, described(spec), flagRows(spec), [ ['OPERANDS', operandRows(spec)], ]), }; } /** * The page of one node of an installed CLI, named after its path with * hyphens (`git-commit`). The synopsis, the options and the operands are the * ones the node's `--help` prints, in the root's dialect. */ export function cliPage(head, path, node, style) { const words = [head, ...path].join(' '); const rows = subcommandRows(node); const listed = listedNode(node, style); let synopsis; let operands; let options; if (style === UsageStyle.ARGPARSE || style === UsageStyle.COBRA) { const [usage, parsedOperands, parsedOptions] = argparseParts(words, listed, rows); synopsis = usage.replace(/^usage: /, ''); operands = parsedOperands; options = parsedOptions; } else { synopsis = usageLine(words, listed, rows, style).replace(/^Usage: /, ''); operands = operandRows(listed); options = flagRows(listed); } return { name: words.replaceAll(' ', '-'), summary: node.description ?? '', sections: sections(synopsis, described(node), options, [ ['OPERANDS', operands.filter(([, does]) => does !== '')], ['COMMANDS', rows], ]), }; } /** * The page of a shell builtin: bash's from its help, else the spec of one * that is a program elsewhere (`man`, `sleep`, `env`). */ function shellPage(name) { if (SHELL_SYNOPSES[name] !== undefined && BASH_BUILTINS.has(name)) return builtinPage(name); const spec = SHELL_SPECS[name] ?? BUILTIN_SPECS[SPEC_OF.get(name) ?? name]; if (SHELL_WORDS.has(name) && spec !== undefined) return specPage(name, spec); return null; } /** * The page of a mounted command, null when no mount registers it. A name has * one spec however many mounts register it, so the first registration is the * page. A wrapper no mount registers (`egrep`) has the page of the program it * runs, as Debian links one to the other. */ function commandPage(name, registry) { for (const mount of registry.allMounts()) { if (mount.prefix === DEV_PREFIX) continue; const cmd = mount.resolveCommand(name); if (cmd !== null) return specPage(name, cmd.spec); } const runs = EXEC_WRAPPERS.get(name); if (runs !== undefined) { const page = commandPage(runs[0], registry); if (page !== null) return { name, summary: page.summary, sections: page.sections }; } return null; } /** The node a hyphenated rest of a page name spells below `node`. */ function subpage(node, rest) { for (const child of node.subcommands) { for (const word of [child.name, ...child.aliases]) { if (rest === word) return [child, [child.name]]; if (rest.startsWith(`${word}-`)) { const deeper = subpage(child, rest.slice(word.length + 1)); if (deeper !== null) return [deeper[0], [child.name, ...deeper[1]]]; } } } return null; } /** * The page an installed CLI has under a name: its head word, or a subcommand * spelled with hyphens (`git-commit`). A head wins over another install's * subcommand spelled the same way. */ function cliLookup(name, registry) { const installs = registry.clis.items(); const exact = installs.get(name); if (exact !== undefined) return cliPage(name, [], exact.cli.spec, exact.cli.spec.usageStyle); for (const [head, install] of installs) { const spec = install.cli.spec; if (name.startsWith(`${head}-`)) { const found = subpage(spec, name.slice(head.length + 1)); if (found !== null) return cliPage(head, found[1], found[0], spec.usageStyle); } } return null; } /** * Every page a name has, in the order `man` prefers them: an installed CLI's, * then a builtin's, then a mounted command's. */ export function pagesOf(name, registry) { return [cliLookup(name, registry), shellPage(name), commandPage(name, registry)].filter((page) => page !== null); } /** The page a name opens, the first of `pagesOf`. */ export function findPage(name, registry) { return pagesOf(name, registry)[0] ?? null; } /** Every page of an installed CLI's tree, the node's first. */ function* cliPages(head, path, node, style) { yield cliPage(head, path, node, style); for (const child of node.subcommands) yield* cliPages(head, [...path, child.name], child, style); } /** Every page of the manual, one per name, sorted by name. */ function allPages(registry) { const pages = new Map(); for (const [head, install] of registry.clis.items()) { const spec = install.cli.spec; for (const page of cliPages(head, [], spec, spec.usageStyle)) { if (page.name === head || !pages.has(page.name)) pages.set(page.name, page); } } const words = new Set([ ...Object.keys(SHELL_SYNOPSES).filter((name) => BASH_BUILTINS.has(name)), ...SHELL_WORDS, ]); for (const mount of registry.allMounts()) { if (mount.prefix !== DEV_PREFIX) for (const cmd of mount.allCommands()) words.add(cmd.name); } for (const [word, [runs]] of EXEC_WRAPPERS) if (words.has(runs)) words.add(word); for (const word of words) { if (pages.has(word)) continue; const found = findPage(word, registry); if (found !== null) pages.set(word, found); } return [...pages].sort(([a], [b]) => compareCodePoints(a, b)).map(([, page]) => page); } /** A header or footer line: one part at each margin, one centered. */ function three(left, center, right, length) { let line = left; if (center !== '') { const start = Math.max(Math.floor((length - center.length + 1) / 2), line.length + 1); line = line.padEnd(start) + center; } return line.padEnd(Math.max(length - right.length, line.length + 1)) + right; } /** * A page laid out as man prints it with justification and hyphenation off: a * header, NAME, each section with its text indented and its tags hanging, * and a footer. Lines stop one short of the terminal width. */ export function renderPage(page, width) { const length = width - 1; const title = `${page.name.toUpperCase()}(${SECTION})`; const text = ' '.repeat(TEXT_INDENT); const tagged = ' '.repeat(TAG_INDENT); const lines = [three(title, MANUAL, title, length), '']; const named = page.summary !== '' ? `${page.name} - ${page.summary}` : page.name; const all = [['NAME', [named]], ...page.sections]; for (const [heading, blocks] of all) { lines.push(heading); blocks.forEach((block, i) => { if (i > 0) lines.push(''); if (typeof block === 'string') { lines.push(...fill(block, length - TEXT_INDENT).map((line) => text + line)); return; } const [tag, does] = block; let body = fill(does, length - TAG_INDENT); const first = body[0]; if (first !== undefined && tag.length < TAG_INDENT - TEXT_INDENT) { lines.push(text + tag.padEnd(TAG_INDENT - TEXT_INDENT) + first); body = body.slice(1); } else { lines.push(text + tag); } lines.push(...body.map((line) => tagged + line)); }); lines.push(''); } lines.push(three(`Mirage ${VERSION}`, '', title, length)); return `${lines.join('\n')}\n`; } /** A page's line in `man -f` and `man -k`. */ function whatis(page) { const named = `${page.name} (${SECTION})`.padEnd(WHATIS_WIDTH); return `${named} - ${page.summary !== '' ? page.summary : '(unknown subject)'}\n`; } /** man's refusal of its own command line: the message, then the hint, status 1. */ function usageError(message) { return result('man', { exitCode: 1, stderr: `man: ${message}\n${HINT}` }); } /** What man says of a command line its options refuse, null when they take it. */ function refusal(parse) { if (parse.unexpectedValue !== null) { const spelled = parse.unexpectedValue.split('=')[0] ?? ''; return `option '${spelled}' doesn't allow an argument`; } if (parse.invalid === null) return null; if (!parse.invalid.startsWith('--')) return `invalid option -- '${parse.invalid}'`; if (parse.candidates.length > 0) { const listed = parse.candidates.map((name) => `'${name}'`).join(' '); return `option '${parse.invalid}' is ambiguous; possibilities: ${listed}`; } return `unrecognized option '${parse.invalid}'`; } /** * Each page asked for and every page it opens. Words after an installed * CLI's head that name its subcommands make one page with it, as * `man git commit` opens git-commit. */ function* lookup(pages, registry) { let i = 0; while (i < pages.length) { const name = pages[i] ?? ''; i += 1; const install = registry.clis.get(name); if (install === null) { yield [name, pagesOf(name, registry)]; continue; } const spec = install.cli.spec; let node = spec; const path = []; while (i < pages.length) { const child = findChild(node, pages[i] ?? ''); if (child === null) break; node = child; path.push(child.name); i += 1; } yield [ name, path.length > 0 ? [cliPage(name, path, node, spec.usageStyle)] : pagesOf(name, registry), ]; } } /** * Run `man`: show the pages asked for, or their summary lines. Status 16 * when a page is missing, as man-db answers, after the pages it did find; 1 * for a command line it refuses. $MANWIDTH or $COLUMNS sets the width. */ export async function handleMan(args, registry, session) { const spec = SHELL_SPECS.man; const parse = parseShellOptions(spec, [...args]); const wrong = refusal(parse); if (wrong !== null) return usageError(wrong); const chosen = parse.flags; const pages = [...parse.operands]; if (chosen['?'] !== undefined) { return result('man', { out: encodeText(renderHelp('man', spec, [], undefined, SYNOPSES.man)) }); } if (chosen.usage !== undefined) { const line = usageLine('man', spec, [], spec.usageStyle, SYNOPSES.man); return result('man', { out: encodeText(`${line}\n`) }); } if (chosen.f !== undefined && chosen.k !== undefined) { return usageError('-f -k : incompatible options'); } if (chosen.f !== undefined || chosen.k !== undefined) { return summaries(pages, registry, chosen.f !== undefined, utf8Locale(envSnapshot(session))); } if (pages.length === 0) return result('man', { exitCode: 1, stderr: NO_PAGE }); let section = ''; if (pages.length > 1 && SECTION_RE.test(pages[0] ?? '')) section = pages.shift() ?? ''; const width = Math.min(envWidth(session, ['MANWIDTH', 'COLUMNS'], MIN_WIDTH, DEFAULT_WIDTH), MAX_WIDTH); const out = []; const errors = []; for (const [name, found] of lookup(pages, registry)) { if (found.length === 0 || (section !== '' && section !== SECTION)) { const where = section !== '' ? ` in section ${section}` : ''; errors.push(`No manual entry for ${name}${where}\n`); continue; } const shown = chosen.a !== undefined ? found : found.slice(0, 1); out.push(...shown.map((page) => renderPage(page, width))); } return result('man', { out: out.length > 0 ? encodeText(out.join('')) : null, exitCode: errors.length > 0 ? 16 : 0, stderr: errors.join(''), }); } /** * `man -f` (each page's summary line) or `man -k` (every page whose name or * summary the word matches, as a regular expression, ignoring case). */ async function summaries(words, registry, isWhatis, utf8) { if (words.length === 0) { return result('man', { exitCode: 1, stderr: `${isWhatis ? 'whatis' : 'apropos'} what?\n` }); } const patterns = []; for (const word of isWhatis ? [] : words) { try { patterns.push(posixRegex(byteView(word, utf8), true, 'posix', true, utf8)); } catch (err) { if (!(err instanceof RegexError)) throw err; const message = `apropos: fatal: regex \`${word}': ${err.message}\n`; return result('man', { exitCode: 2, stderr: message }); } } const found = []; const errors = []; const every = isWhatis ? [] : allPages(registry); for (const [index, word] of words.entries()) { let hits = []; if (isWhatis) { const page = findPage(word, registry); hits = page !== null ? [page] : []; } else { const pattern = patterns[index]; if (pattern !== undefined) { for (const page of every) if (await matches(pattern, page, utf8)) hits.push(page); } } if (hits.length === 0) errors.push(`${word}: nothing appropriate.\n`); for (const page of hits) if (!found.some((seen) => seen.name === page.name)) found.push(page); } if (!isWhatis) found.sort((a, b) => compareCodePoints(a.name, b.name)); return result('man', { out: found.length > 0 ? encodeText(found.map(whatis).join('')) : null, exitCode: found.length > 0 ? 0 : 16, stderr: errors.join(''), }); } /** Whether a keyword matches a page's name or its summary. */ async function matches(pattern, page, utf8) { for (const text of [page.name, page.summary]) { if ((await pattern.search(byteView(text, utf8))) !== null) return true; } return false; } /** The `man` arm. */ export function manBuiltin(call) { return handleMan([...call.argv.args], call.registry, call.context.session); } //# sourceMappingURL=man.js.map