UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

270 lines • 10.9 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 { atoi } from "../../../../commands/builtin/utils/strtol.js"; import { compileSpec } from "../../../../commands/spec/compile.js"; import { SHELL_SPECS } from "../../../../commands/spec/shell.js"; import { versionLine } from "../../../../commands/spec/standard.js"; import { SHELL_SYNOPSES } from "../../../../commands/spec/synopsis.js"; import { runs } from "../../../../policy/match/allow.js"; import { decodeText, encodeText } from "../../../../shell/bytes.js"; import { fnmatch } from "../../../../utils/fnmatch.js"; import { compareCodePoints } from "../../../../utils/sort.js"; import { BASH_BUILTINS } from "../../../lookup/constants.js"; import { visibleEnv } from "../../../session/state.js"; import { scanOptions } from "../getopt.js"; import { builtinError, builtinUsage, result } from "../shared.js"; import { DEFAULT_COLUMNS, DESCRIPTION, GUIDE, MAX_CELL, MIN_COLUMNS, OPERAND_HELP, TEXT_WIDTH, } from "./constants.js"; const INDENT = ' '; // Every topic, in the order the listing and a pattern's matches show. export const TOPICS = Object.keys(SHELL_SYNOPSES).sort(compareCodePoints); function specOf(name) { const spec = SHELL_SPECS[name]; if (spec === undefined) throw new Error(`no shell spec: ${name}`); return spec; } /** A spec's free text as paragraphs, split at its blank lines. */ export function paragraphs(text) { if (text === null || text === '') return []; return text .split('\n\n') .filter((part) => part.trim() !== '') .map((part) => part.trim().split(/\s+/).join(' ')); } /** The one line a spec's description opens with. */ export function summary(spec) { return paragraphs(spec.description)[0] ?? ''; } /** * Each option of a builtin, as its help lists it: the letter and the name of * its value, and what it does. */ export function optionRows(spec) { return compileSpec(spec).options.map((opt) => { let label = opt.names[0] ?? ''; if (opt.action !== 'store_true' && opt.action !== 'count' && opt.metavar) { label += ` ${opt.metavar}`; } return [label, opt.help ?? '']; }); } /** * Where a word may break across lines: after a hyphen that follows a letter * or digit, so `read-only` breaks and `--all` does not. */ function breakPoints(word) { const points = []; for (let i = 1; i < word.length - 1; i += 1) { if (word[i] === '-' && /^[\p{L}\p{N}]$/u.test(word[i - 1] ?? '')) points.push(i + 1); } return points; } /** * Lay text out in lines of at most `width` characters. Words go on a line * while they fit; a word that does not fit breaks after its last hyphen that * still does, and a word longer than a line with nowhere to break runs past * it. This is how a manual page fills text with justification and * hyphenation off. */ export function fill(text, width) { const lines = []; let line = ''; for (const whole of text.split(/\s+/).filter((w) => w !== '')) { let word = whole; while (word !== '') { const room = width - line.length - (line !== '' ? 1 : 0); if (word.length <= room) { line = line !== '' ? `${line} ${word}` : word; break; } const cut = Math.max(0, ...breakPoints(word).filter((p) => p <= room)); if (cut > 0) { lines.push(line !== '' ? `${line} ${word.slice(0, cut)}` : word.slice(0, cut)); line = ''; word = word.slice(cut); } else if (line !== '') { lines.push(line); line = ''; } else { line = word; word = ''; } } } if (line !== '') lines.push(line); return lines; } /** A paragraph of a topic's help, filled and indented. */ function indented(text) { return fill(text, TEXT_WIDTH - INDENT.length).map((line) => INDENT + line); } /** * A topic's help below its synopsis line: the summary, then the description, * the options and the exit status, each set off by an indented blank line. */ function topicBody(spec) { const [lead = '', ...text] = paragraphs(spec.description); const lines = [INDENT + lead]; for (const paragraph of text) lines.push(INDENT, ...indented(paragraph)); const rows = optionRows(spec); if (rows.length > 0) { lines.push(INDENT, `${INDENT}Options:`); lines.push(...rows.map(([flag, does]) => ` ${flag}\t${does}`)); } if (spec.epilog !== null && spec.epilog !== '') { lines.push(INDENT, `${INDENT}Exit Status:`, ...indented(spec.epilog)); } return lines; } /** What `help NAME` and `NAME --help` print. */ export function topicHelp(name) { return `${[`${name}: ${SHELL_SYNOPSES[name] ?? ''}`, ...topicBody(specOf(name))].join('\n')}\n`; } /** What `help -m NAME` prints: the help laid out in a manual page's sections. */ function manual(name) { const spec = specOf(name); const lines = [ 'NAME', `${INDENT}${name} - ${summary(spec)}`, '', 'SYNOPSIS', INDENT + (SHELL_SYNOPSES[name] ?? ''), '', 'DESCRIPTION', ...topicBody(spec), '', 'SEE ALSO', `${INDENT}bash(1)`, '', 'IMPLEMENTATION', INDENT + versionLine('bash').replace(/\n$/, ''), '', ]; return `${lines.join('\n')}\n`; } /** * The width the first of `names` sets, read as C's `atoi` reads it, passing * over one under `least`; `fallback` when none sets one. */ export function envWidth(session, names, least, fallback) { const env = visibleEnv(session); for (const name of names) { const width = atoi(env[name] ?? ''); if (width >= least) return width; } return fallback; } /** Whether the session may not run a builtin at all, which the listing marks with a star. */ export function disabled(name, session) { return BASH_BUILTINS.has(name) && !runs(name, session.commands ?? null); } /** One entry of the listing: its mark and synopsis, cut with `>` once the synopsis reaches `room` characters. */ function cell(name, session, room) { const mark = disabled(name, session) ? '*' : ' '; let text = SHELL_SYNOPSES[name] ?? ''; if (text.length >= room) text = `${text.slice(0, Math.max(room, 0))}>`; return mark + text; } /** Every topic's synopsis in two columns, under the shell's name and a guide to the listing. */ function listing(session) { const columns = envWidth(session, ['COLUMNS'], MIN_COLUMNS, DEFAULT_COLUMNS); const width = Math.min(Math.floor(columns / 2), MAX_CELL); const rows = Math.floor((TOPICS.length + 1) / 2); const lines = [versionLine('bash').replace(/\n$/, ''), DESCRIPTION, ...GUIDE, '']; for (let i = 0; i < rows; i += 1) { const left = cell(TOPICS[i] ?? '', session, width - 3); const right = TOPICS[i + rows]; lines.push(right !== undefined ? left.padEnd(width) + cell(right, session, width - 4) : left); } return `${lines.join('\n')}\n`; } /** Whether a pattern is a glob, which titles the output with the patterns asked for. */ function globbed(pattern) { if (pattern.includes('*') || pattern.includes('?')) return true; const start = pattern.indexOf('['); return start >= 0 && pattern.slice(start + 1).includes(']'); } /** The topics a pattern names: the one it spells exactly, else every one it begins or matches as a glob. */ export function matching(pattern) { if (SHELL_SYNOPSES[pattern] !== undefined) return [pattern]; return TOPICS.filter((name) => name.startsWith(pattern) || fnmatch(name, pattern)); } /** * A builtin's answer to `--help` as its first word: its help, and status 2. * Null for a word that reads `--help` as an operand, and for any word that is * no builtin. */ export function flagHelp(name) { if (SHELL_SYNOPSES[name] === undefined || !BASH_BUILTINS.has(name) || OPERAND_HELP.has(name)) { return null; } return result(name, { out: encodeText(topicHelp(name)), exitCode: 2 }); } /** * The `help` arm: the listing, or what each pattern names. `-d` wins over * `-m` and `-m` over `-s`. A pattern that matches nothing is passed over; * only when none matches is there an error, and it names the last. */ export function helpBuiltin(call) { const session = call.context.session; const scan = scanOptions([...call.argv.args], 'dms'); if (scan.bad !== null) { const message = decodeText(builtinError('help', `${scan.bad}: invalid option`)); return Promise.resolve(result('help', { exitCode: 2, stderr: message + builtinUsage('help') })); } if (scan.operands.length === 0) { return Promise.resolve(result('help', { out: encodeText(listing(session)) })); } const mode = ['d', 'm', 's'].find((m) => scan.letters.includes(m)) ?? null; const out = []; const first = scan.operands[0] ?? ''; if (globbed(first)) { const plural = scan.operands.length > 1 ? 's' : ''; out.push(`Shell commands matching keyword${plural} \`${scan.operands.join(', ')}'\n\n`); } let found = false; for (const pattern of scan.operands) { for (const name of matching(pattern)) { found = true; if (mode === 'd') out.push(`${name} - ${summary(specOf(name))}\n`); else if (mode === 'm') out.push(manual(name)); else if (mode === 's') out.push(`${name}: ${SHELL_SYNOPSES[name] ?? ''}\n`); else out.push(topicHelp(name)); } } if (found) return Promise.resolve(result('help', { out: encodeText(out.join('')) })); const last = scan.operands[scan.operands.length - 1] ?? ''; const message = decodeText(builtinError('help', `no help topics match \`${last}'. Try \`help help' or \`man -k ${last}' or \`info ${last}'.`)); return Promise.resolve(result('help', { out: out.length > 0 ? encodeText(out.join('')) : null, exitCode: 1, stderr: message, })); } //# sourceMappingURL=help.js.map