UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

244 lines • 10.8 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 { compileSpec, argumentDest, optionSpellings, positionalName, positionalRequired, } from "./compile.js"; import { ARG_PLACEHOLDER } from "./constants.js"; import { UsageStyle } from "./types.js"; import { compareCodePoints } from "../../utils/sort.js"; import { rstripNewlines } from "../../utils/text.js"; /** * The bare name of an option's value, declared or derived. * * clap derives one from the long spelling when the author declares no * `value_name`: dashes to underscores, uppercased. Deriving it the same way * means only the options that actually override it have to say so. */ export function optionMetavar(opt) { if (opt.metavar !== null) return opt.metavar; const spelling = argumentDest(opt); return spelling.replace(/^-+/, '').replaceAll('-', '_').toUpperCase(); } /** * One operand's slot in a clap usage line. Required slots take angle brackets * and optional ones square, which is the only thing clap's usage line says * about arity besides the trailing ellipsis on a variadic. */ export function operandSlot(operand, ellipsis = false) { const name = positionalName(operand) === '' ? ARG_PLACEHOLDER : positionalName(operand); const slot = positionalRequired(operand) ? `<${name}>` : `[${name}]`; return ellipsis ? `${slot}...` : slot; } function valueLabel(opt) { if (opt.action === 'store_true' || opt.action === 'count') return ''; const value = `<${opt.metavar ?? (opt.type === 'path' ? 'path' : 'text')}>`; if (opt.valueTypes.length > 0) return ` <name> ${value}`; return ` ${value}`.repeat(typeof opt.nargs === 'number' ? opt.nargs : 1); } function flagDisplay(opt) { return opt.names.join(', ') + valueLabel(opt); } /** Display rows [flag spelling, description] for a spec's options. */ export function flagRows(spec) { return compileSpec(spec).options.map((o) => [flagDisplay(o), o.help ?? '']); } /** An operand's name in help: the one its spec gives, else its type. */ function operandLabel(operand) { return positionalName(operand) || (operand.type === 'path' ? 'PATH' : 'ARG'); } /** * Display rows [operand name, description] for the operands a spec explains, * the ones with help of their own. */ export function operandRows(spec) { const cs = compileSpec(spec); return [...cs.positional, ...(cs.rest === null ? [] : [cs.rest])] .filter((operand) => Boolean(operand.help)) .map((operand) => [operandLabel(operand), operand.help ?? '']); } /** * One operand's placeholder outside the clap dialect. * * A spec that named the slot gets that name, which is what argparse prints * too (it renders the dest, not a generic word); a spec that did not falls * back to the type. Only `gh` names one outside clap today, so this is the * difference between `gh api [flags] <text>` and upstream's * `gh api <endpoint>`. */ function slot(operand) { if (positionalName(operand) !== '') return `<${positionalName(operand)}>`; return operand.type === 'path' ? '<path>' : '<text>'; } /** * Render one command's help; a CLI group is the same shape plus a Commands * section. `subcommands` carries (name, one-line help) rows for a CLI * group node; when given, the usage line reads `<command> [<args>]` * instead of the operand slots. */ export function usageLine(name, spec, subcommands, style, synopsis) { // The builtin that mimics a real program answers with that program's // own first line rather than one synthesized from its slots. if (synopsis !== undefined) return `Usage: ${synopsis}`; const cs = compileSpec(spec); const clap = style === UsageStyle.CLAP; const bits = [name]; if (compileSpec(spec).options.length > 0) bits.push(clap ? '[OPTIONS]' : '[flags]'); if (subcommands.length > 0) bits.push(clap ? '<COMMAND>' : '<command> [<args>]'); for (const op of cs.positional) { bits.push(clap ? operandSlot(op) : slot(op)); } if (cs.rest !== null) { if (clap) bits.push(operandSlot(cs.rest, !positionalRequired(cs.rest))); else bits.push(`[${slot(cs.rest)}...]`); } return `Usage: ${bits.join(' ')}`; } export function renderHelp(name, spec, subcommands = [], style = UsageStyle.ARGPARSE, synopsis) { const clap = style === UsageStyle.CLAP; const lines = []; if (spec.description === null || spec.description === '') { lines.push(name); } else if (clap) { lines.push(spec.description); } else { lines.push(`${name}: ${spec.description}`); } lines.push(''); lines.push(usageLine(name, spec, subcommands, style, synopsis)); if (subcommands.length > 0) { lines.push(''); lines.push('Commands:'); const width = Math.max(...subcommands.map(([sub]) => sub.length)); // clap prints subcommands in the order the program declares them, which is // a deliberate ordering by an author rather than an alphabet, so re-sorting // would lose information. const subRows = clap ? subcommands : [...subcommands].sort((a, b) => compareCodePoints(a[0], b[0])); for (const [sub, desc] of subRows) { const first = desc.split('\n')[0] ?? ''; lines.push(first === '' ? ` ${sub}` : ` ${sub.padEnd(width, ' ')} ${first}`); } } if (compileSpec(spec).options.length > 0) { lines.push(''); lines.push(clap ? 'Options:' : 'Flags:'); const rows = flagRows(spec); const width = Math.max(...rows.map(([flag]) => flag.length)); for (const [flag, desc] of rows) { lines.push(desc === '' ? ` ${flag}` : ` ${flag.padEnd(width, ' ')} ${desc}`); } } const operands = spec.arguments.filter((argument) => !argument.names[0]?.startsWith('-') && argument.help); if (operands.length > 0) { lines.push('', 'Arguments:'); const width = Math.max(...operands.map((operand) => slot(operand).length)); for (const operand of operands) lines.push(` ${slot(operand).padEnd(width)} ${operand.help ?? ''}`); } if (spec.epilog !== null && spec.epilog !== '') { lines.push(''); lines.push(rstripNewlines(spec.epilog)); } return lines.join('\n') + '\n'; } // clap's wording for a token it has no option for. One wording for long and // short alike, unlike git's option/switch split, and it names the token as // typed. Probed against ntn 0.21.9. export function clapUnexpectedArgument(token) { return `error: unexpected argument '${token}' found`; } // A group-level refusal in clap's shape: message, usage line, footer. clap // answers with the one usage line, not the whole help page git prints, and it // does so at every level of the tree. This lives beside the renderer rather // than beside the leaf refusals because the walk calls it, and the walk cannot // reach a module that imports the workspace. export function clapGroupRefusal(name, spec, subcommands, message) { const usage = usageLine(name, spec, subcommands, UsageStyle.CLAP); return `${message}\n\n${usage}\n\nFor more information, try '--help'.\n`; } /** * The usage line and the operand and option rows of a CLI's argparse-shaped * help, before they are laid out. */ export function argparseParts(name, spec, subcommands = []) { const cs = compileSpec(spec); const usage = [name]; const rows = []; for (const opt of compileSpec(spec).options) { const value = opt.metavar ?? (opt.choices.length > 0 ? `{${opt.choices.join(',')}}` : optionMetavar(opt)); let suffix = opt.action === 'store_true' || opt.action === 'count' ? '' : ` ${value}`; if (opt.valueTypes.length > 0) suffix = ` NAME ${value}`; else if (typeof opt.nargs === 'number') suffix = ` ${value}`.repeat(opt.nargs); if (opt.nargs === '?') suffix = opt.attachedOnly ? `[=${value}]` : ` [${value}]`; const flags = opt.names.map((flag) => `${flag}${suffix}`).join(', '); const slot = `${optionSpellings(opt)[0] ?? argumentDest(opt)}${suffix}`; usage.push(opt.required ? slot : `[${slot}]`); const details = [opt.help ?? '']; if (opt.default !== null) details.push(`(default: ${opt.default})`); if (opt.env !== null) details.push(`(env: ${opt.env})`); if (opt.required) details.push('(required)'); if (opt.action === 'append' || opt.action === 'extend') details.push('(repeatable)'); rows.push([flags, details.filter(Boolean).join(' ')]); } const operands = []; for (const operand of [...cs.positional, ...(cs.rest === null ? [] : [cs.rest])]) { const label = operandLabel(operand); const slot = label + (operand === cs.rest ? ' ...' : ''); usage.push(positionalRequired(operand) ? slot : `[${slot}]`); operands.push([label, operand.help ?? (operand.type === 'path' ? 'Virtual path' : '')]); } if (subcommands.length > 0) usage.push(`{${subcommands.map(([sub]) => sub).join(',')}} ...`); return [`usage: ${usage.join(' ')}`, operands, rows]; } /** Render the declared CLI grammar without serializing internal spec objects. */ export function argparseHelp(name, spec, subcommands = []) { const [usage, operands, rows] = argparseParts(name, spec, subcommands); const lines = [usage]; if (spec.description) lines.push('', spec.description); for (const [title, table] of [ ['positional arguments:', operands], ['commands:', subcommands], ['options:', rows], ]) { if (table.length === 0) continue; lines.push('', title); const width = Math.max(...table.map(([label]) => label.length)); for (const [label, description] of table) lines.push(` ${label.padEnd(width)} ${description}`.trimEnd()); } if (spec.epilog) lines.push('', spec.epilog.replace(/\n+$/, '')); return lines.join('\n') + '\n'; } //# sourceMappingURL=help.js.map