@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
244 lines • 10.8 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 { 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