@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
787 lines • 34.5 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 { flagOccurrences } from "../spec/flag_view.js";
import { HELP_OPTION, FLOAT_VALUE, INT_VALUE, NEGATIVE_NUMBER } from "../spec/constants.js";
import { compileSpec, expandLong } from "../spec/compile.js";
import { argparseHelp, clapGroupRefusal, clapUnexpectedArgument, renderHelp } from "../spec/help.js";
import { CommandSpec, UsageStyle, Argument } from "../spec/types.js";
import { PathSpec } from "../../types.js";
import { WalkResult } from "./types.js";
import { CLAP_EXIT, GIT_SYNOPSES, HELP_EXIT, USAGE_EXIT } from "./constants.js";
import { gitOptionRefusal, HELP_SWITCH } from "./refusal.js";
import { encodeText } from "../../shell/bytes.js";
/** A subcommand's row label: `name (alias, ...)` like argparse. */
function verbDisplay(child) {
return child.aliases.length > 0 ? `${child.name} (${child.aliases.join(', ')})` : child.name;
}
/** The subcommand a word names, by canonical name or alias. */
export function findChild(node, word) {
return node.subcommands.find((c) => c.name === word || c.aliases.includes(word)) ?? null;
}
/**
* Every `Argument.env` variable a program tree reads.
*
* The env-plane fill step asks this per installed head word on the
* line, so a managed name a CLI reads from the environment joins the
* fetch set even though no `$NAME` appears in the line's text.
*/
export function envNames(node) {
const out = new Set();
for (const opt of compileSpec(node).options) {
if (opt.env !== null)
out.add(opt.env);
}
for (const child of node.subcommands) {
for (const name of envNames(child))
out.add(name);
}
return out;
}
/**
* Env names on the verb paths the line's words could select.
*
* The words prune the tree: a subcommand joins only when some word
* spells its name or an alias, recursively, so a bare head reads the
* root's env names and `ntn api get` adds exactly the api and get
* nodes. A word doubling as an operand over-selects, which costs one
* fetch; a verb can never hide, because dispatch only runs a verb the
* line spells. Null means a word no static read can spell (an
* expansion), where the whole tree is the only safe answer.
*/
export function invokedEnvNames(spec, words) {
if (words === null)
return envNames(spec);
const out = new Set();
for (const opt of compileSpec(spec).options) {
if (opt.env !== null)
out.add(opt.env);
}
for (const child of spec.subcommands) {
if (words.has(child.name) || child.aliases.some((alias) => words.has(alias))) {
for (const name of invokedEnvNames(child, words))
out.add(name);
}
}
return out;
}
/**
* The spelling one dash token certainly supplies, and its width.
*
* Mirrors the exact-token arms the walk and the flat parser share -- an
* attached value, a value in the next word, a bare boolean -- and
* answers null for anything subtler (a cluster, an abbreviation, an
* undeclared spelling, a long given a value it refuses), where the
* caller must stop claiming anything.
*/
function suppliedOption(cs, token, following) {
const next = following[0];
if (token.startsWith('--')) {
const eq = token.indexOf('=');
const spelling = eq === -1 ? token : token.slice(0, eq);
const width = cs.nargsByDest.get(cs.destOf(spelling));
if (width === 1 && eq !== -1)
return [spelling, 1];
if (width !== undefined)
return eq === -1 && following.length >= width ? [spelling, width + 1] : null;
if (cs.longOptionalSpellings.has(spelling)) {
const detached = cs.detachedOptionalSpellings.has(spelling) && optionalValue(next);
return [spelling, eq === -1 && detached ? 2 : 1];
}
if (cs.longBoolSpellings.has(spelling))
return eq === -1 ? [spelling, 1] : null;
if (cs.longValueSpellings.has(spelling)) {
if (eq !== -1)
return [spelling, 1];
return next !== undefined ? [spelling, 2] : null;
}
return null;
}
for (const vf of cs.attachSpellings) {
if (token.startsWith(vf) && token.length > vf.length)
return [vf, 1];
}
for (const vf of cs.valueSpellings) {
if (token !== vf && !(token.startsWith(vf) && token.length > vf.length))
continue;
const width = cs.nargsByDest.get(cs.destOf(vf));
if (width !== undefined) {
const needed = width - (token === vf ? 0 : 1);
return following.length >= needed ? [vf, needed + 1] : null;
}
if (token === vf)
return next !== undefined ? [vf, 2] : null;
if (token.startsWith(vf) && token.length > vf.length)
return [vf, 1];
}
if (cs.detachedOptionalSpellings.has(token))
return [token, optionalValue(next) ? 2 : 1];
if (cs.boolSpellings.has(token))
return [token, 1];
return null;
}
function optionalValue(value) {
return (value !== undefined && (!value.startsWith('-') || value === '-' || NEGATIVE_NUMBER.test(value)));
}
/**
* Variables every visited reader of which was supplied. `carriers` is
* each visited level's `envByDest` table in walk order; `supplied` the
* `level:destination` keys the line certainly fills.
*/
function claimed(carriers, supplied) {
const won = new Set();
const blocked = new Set();
carriers.forEach((table, level) => {
for (const [dest, variable] of table) {
if (supplied.has(`${String(level)}:${dest}`))
won.add(variable);
else
blocked.add(variable);
}
});
for (const variable of blocked)
won.delete(variable);
return won;
}
/**
* Env names whose every reader on the walked path is supplied.
*
* The parser never reads `Argument.env` for a destination the line
* already fills (typed outranks environment), so a supplied option's
* managed variable is not a read and must not fetch: a dead source
* would otherwise fail a line that never consults it. Tracking is by
* destination, never by bare name: two options may declare one
* variable, and a variable shared by a supplied and an unsupplied
* destination stays a read, because the unsupplied one still falls
* back to it. Presence is claimed only where consumption is certain,
* walking level by level the way `walk` does and matching only the
* exact-token forms. Anything subtler stops the scan -- keeping what
* was proven for a word that only ends option parsing (an operand
* under a remainder leaf, a verb that matches nothing; `--` also
* drops every variable readable below the group, since the walk keeps
* descending after it), and keeping nothing for a word whose
* consumption is in doubt (a cluster, an abbreviation, `--help`) --
* so a wrong guess can only over-fetch, never skip a real read.
*/
export function suppliedEnvNames(spec, args) {
const supplied = new Set();
let node = spec;
let cs = compileSpec(node);
const carriers = [cs.envByDest];
let i = 0;
while (i < args.length) {
const token = args[i];
if (token === undefined)
break;
if (token === '--') {
const out = claimed(carriers, supplied);
for (const child of node.subcommands) {
for (const below of envNames(child))
out.delete(below);
}
return out;
}
if (token.startsWith('-') && token !== '-') {
if (token === '--help' || token.startsWith('--help='))
return new Set();
const hit = suppliedOption(cs, token, args.slice(i + 1));
if (hit === null)
return new Set();
const [spelling, consumed] = hit;
supplied.add(`${String(carriers.length - 1)}:${cs.destOf(spelling)}`);
i += consumed;
continue;
}
if (node.subcommands.length === 0) {
if (cs.remainder)
return claimed(carriers, supplied);
i += 1;
continue;
}
const child = findChild(node, token);
if (child === null)
return claimed(carriers, supplied);
node = child;
cs = compileSpec(node);
carriers.push(cs.envByDest);
i += 1;
}
return claimed(carriers, supplied);
}
/**
* Descend a tree by verb words, null if a word names no subcommand.
*
* Returns the node and its canonical path, so an alias renders under the
* name it resolves to, the attribution rule `walk` uses. This is
* introspection only (`man`): no options are parsed and no usage error is
* produced, so a caller gets the node or nothing.
*/
export function findNode(spec, verbs) {
let node = spec;
const path = [];
for (const word of verbs) {
const child = findChild(node, word);
if (child === null)
return null;
node = child;
path.push(child.name);
}
return { node, path };
}
/**
* True when the node parses its own command line instead of mirage.
*
* A script root that declares no grammar is the only such node: the
* embedded program is the parser, so its flags are not mirage's to
* recognize, and a generated help page would document nothing. Mirage
* forwards the whole line to it (a pass-through rest operand) and leaves
* `--help` to the program. A script root that does declare options or
* operands opts back into the ordinary machinery, which then renders
* truthful help and refuses undeclared flags.
*/
export function ownsArgv(cli) {
return cli.script !== null && cli.spec.arguments.length === 0;
}
/**
* A group node's help: the ordinary command help plus Commands rows.
* One renderer serves leaves and groups; the same text serves `--help`
* (stdout, exit 0) and the bare-group refusal (stdout, exit 1, matching
* git). `name` is the full display path as typed ('gws gmail'), so a
* renamed install renders its own spelling.
*/
export function nodeHelp(name, node, style = UsageStyle.ARGPARSE) {
if (style === UsageStyle.ARGPARSE || style === UsageStyle.COBRA)
return argparseHelp(name, listedNode(node, style), subcommandRows(node));
return renderHelp(name, listedNode(node, style), subcommandRows(node), style);
}
/** The node's child rows, as the renderer lists them. */
export function subcommandRows(node) {
return node.subcommands.map((child) => [verbDisplay(child), child.description ?? '']);
}
// The node as the renderer shows it, with `--help` filled in. --help is a
// registered option everywhere (argparse add_help, click add_help_option,
// withHelpSupport for leaves), so the listing shows it unless the node
// declares its own or answers the flag itself (ownsArgv), where advertising it
// would promise a page mirage no longer renders. A refusal renders the same
// node a help page would, or its usage line would disagree with `--help`'s.
// It is the grammar alone: a rebuilt CommandSpec is validated again, and the
// added `--help` would collide with a child that declares its own.
export function listedNode(node, style = UsageStyle.ARGPARSE) {
if (!node.addHelp || node.arguments.some((argument) => argument.names.includes('--help')))
return node;
const names = (style === UsageStyle.ARGPARSE || style === UsageStyle.COBRA) &&
!node.arguments.some((argument) => argument.names.includes('-h'))
? ['-h', '--help']
: ['--help'];
const help = new Argument(names, { action: 'store_true', help: HELP_OPTION.help });
const spec = node;
return new CommandSpec({ ...spec, arguments: [...node.arguments, help] });
}
/**
* Group-level option refusal, in the dialect the CLI declares. git answers an
* unknown option in parse-options' words and its usage block (on stdout for
* `-h`), and the bare `git` with its one synopsis; both exit 129. The bare
* `git -h` prints the help. clap answers
* with the message, the one usage line and a footer pointing at --help, and
* exits 2, at every level of the tree; the exit code is the group's just as
* much as the leaf's, so reading the style here is what keeps `ntn --bogus`
* and `ntn pages get --bogus` from disagreeing.
*
* `token` is the offending token when the refusal is an unrecognized option,
* which clap and git word their own way; it is absent for the refusals whose
* wording the dialects share.
*/
function usageError(name, node, message, style, token) {
if (style === UsageStyle.CLAP) {
const first = token === undefined ? message : clapUnexpectedArgument(token);
return new WalkResult({
output: encodeText(clapGroupRefusal(name, listedNode(node, style), subcommandRows(node), first)),
stream: 'stderr',
exitCode: CLAP_EXIT,
});
}
if (style === UsageStyle.GIT && token !== undefined) {
const space = name.indexOf(' ');
const path = space === -1 ? '' : name.slice(space + 1);
// `git -h` is git's own help, as `git --help` is, and exits 0.
if (path === '' && token === HELP_SWITCH) {
return new WalkResult({ output: encodeText(nodeHelp(name, node, style)) });
}
if (path === '') {
const synopsis = GIT_SYNOPSES.get('')?.[0] ?? '';
return new WalkResult({
output: encodeText(`unknown option: ${token}\nusage: ${synopsis}\n`),
stream: 'stderr',
exitCode: USAGE_EXIT,
});
}
const [shown, refused] = gitOptionRefusal(token, path, node);
return new WalkResult({
output: encodeText(shown !== '' ? shown : refused),
stream: shown !== '' ? 'stdout' : 'stderr',
exitCode: shown !== '' ? HELP_EXIT : USAGE_EXIT,
});
}
return new WalkResult({
output: encodeText(`${message}\n\n${nodeHelp(name, node, style)}`),
stream: 'stderr',
exitCode: USAGE_EXIT,
});
}
/** git's unknown-command refusal, with the group path in the noun. */
function unknownVerb(head, name, word) {
return new WalkResult({
output: encodeText(`${head}: '${word}' is not a ${name} command. See '${name} --help'.\n`),
stream: 'stderr',
exitCode: 1,
});
}
/** Record a boolean occurrence under its canonical dashed spelling. */
function recordBool(flags, cs, spelling) {
const dest = cs.destOf(spelling);
if (cs.countDests.has(dest)) {
const prev = flags[dest];
flags[dest] = typeof prev === 'number' ? prev + 1 : 1;
}
else {
flags[dest] = true;
}
}
/**
* Record a value occurrence. Directory changes are resolved in occurrence
* order by resolveGroupPaths after the node has been scanned.
*/
function recordValue(flags, cs, spelling, value) {
const dest = cs.destOf(spelling);
flagOccurrences(flags).push([dest, value]);
if (cs.multipleDests.has(dest)) {
const prev = flags[dest];
if (Array.isArray(prev)) {
flags[dest] = [...prev, value];
}
else {
flags[dest] = [value];
}
}
else {
flags[dest] = value;
}
}
function recordValues(flags, cs, spelling, values) {
const dest = cs.destOf(spelling);
for (const value of values)
flagOccurrences(flags).push([dest, value]);
const previous = flags[dest];
flags[dest] =
cs.multipleDests.has(dest) && Array.isArray(previous) ? [...previous, ...values] : [...values];
}
/**
* Match a whole short token against declared spellings. Mirrors the flat
* parser's precedence before cluster splitting: attached values on
* attach-capable spellings, then value spellings (exact token wants the
* next word; longer token carries an attached value), then an exact
* boolean spelling. Multi-char shorts like find's `-name` only match
* here. Returns null to fall through to the single-char cluster loop,
* else [tokens consumed, refusal].
*/
function matchShort(name, node, cs, flags, token, following, style) {
const nextToken = following[0];
for (const vf of cs.attachSpellings) {
if (token.startsWith(vf) && token.length > vf.length) {
recordValue(flags, cs, vf, token.slice(vf.length));
return [1, null];
}
}
for (const vf of cs.valueSpellings) {
if (token !== vf && !(token.startsWith(vf) && token.length > vf.length))
continue;
const arity = cs.nargsByDest.get(cs.destOf(vf));
const attached = token.slice(vf.length);
const remaining = (arity ?? 1) - (attached === '' ? 0 : 1);
if (following.length < remaining)
return [0, usageError(name, node, `error: option '${vf}' requires a value`, style)];
const values = [...(attached === '' ? [] : [attached]), ...following.slice(0, remaining)];
if (arity !== undefined)
recordValues(flags, cs, vf, values);
else
recordValue(flags, cs, vf, values[0] ?? '');
return [remaining + 1, null];
}
if (cs.detachedOptionalSpellings.has(token) && optionalValue(nextToken)) {
recordValue(flags, cs, token, nextToken);
return [2, null];
}
if (cs.boolSpellings.has(token)) {
recordBool(flags, cs, token);
return [1, null];
}
return null;
}
/**
* Prefix-expand a long spelling at a group level. The declared tables
* match first; the injected `--help` joins the candidate pool when the
* node does not declare its own, because it is a registered option
* everywhere else (argparse and getopt_long both expand `--hel` to it).
*/
function expandGroupLong(node, cs, spelling) {
const candidates = expandLong(cs, spelling);
if (node.addHelp &&
'--help'.startsWith(spelling) &&
spelling.length > 2 &&
!candidates.includes('--help') &&
!cs.dest.has('--help')) {
return [...candidates, '--help'];
}
return candidates;
}
/**
* Resolve PATH-typed group values against the working directory.
*
* A group option declared `type: 'path'` has to mean what it means on a leaf,
* or the type is a lie at exactly one level of the tree. The flat parser
* resolves PATH values right after defaults land, so a `-C` that defaults to
* `'.'` becomes the session cwd and a relative `-C build` becomes absolute; do
* the same here rather than handing a leaf a raw relative string it has no cwd
* to interpret.
*
* Keep the typed spelling and its walk verdict, just as leaf PATH values do.
* The operand base resolves first; other path options are relative to it.
*/
function resolveGroupPaths(cs, flags, cwd, bases) {
let base = cwd;
if (cs.baseDest !== null) {
const value = flags[cs.baseDest];
if (typeof value === 'string') {
const values = flagOccurrences(flags)
.filter(([name]) => name === cs.baseDest)
.map(([, value]) => value)
.filter((value) => typeof value === 'string');
let scope = PathSpec.fromStrPath('.', undefined, cwd);
for (const word of values.length > 0 ? values : [value]) {
scope = PathSpec.fromStrPath(word || '.', undefined, scope);
bases.push(scope);
}
flags[cs.baseDest] = scope;
base = scope;
}
}
for (const [dest, kind] of cs.kindByDest) {
if (kind !== 'path' || !(dest in flags) || dest === cs.baseDest)
continue;
const value = flags[dest];
if (Array.isArray(value)) {
const kinds = cs.valueTypesByDest.get(dest) ?? ['path'];
flags[dest] = value.map((part, index) => kinds[index % kinds.length] === 'path' ? PathSpec.fromStrPath(part, undefined, base) : part);
}
else if (typeof value === 'string')
flags[dest] = PathSpec.fromStrPath(value, undefined, base);
}
}
/**
* Apply a node's declarative option rules after its scan: the environment
* lands first, then defaults, then PATH values resolve and choices and
* required are enforced, the same order the flat parser uses (parser.ts) --
* an option's declared variable outranks its default, yields to anything
* the line typed, and gets the same coercion, choices test and required
* credit a typed value does. Returns a rendered refusal or null when
* satisfied.
*/
function finishNode(name, node, cs, flags, cwd, style, bases, env, deferEnv) {
for (const [dest, variable] of cs.envByDest) {
if (dest in flags)
continue;
const supplied = env?.[variable];
if (supplied === undefined || supplied === '')
continue;
flags[dest] = cs.multipleDests.has(dest) ? [supplied] : supplied;
}
for (const [dest, value] of cs.defaults) {
if (!(dest in flags)) {
flags[dest] = cs.multipleDests.has(dest) ? [value] : value;
}
}
resolveGroupPaths(cs, flags, cwd, bases);
// Numeric-typed values before choices, argparse's order; wording is
// git's parse-options refusal (`--depth` on a non-integer), one phrase
// for int and float alike.
for (const [dests, pattern] of [
[cs.intDests, INT_VALUE],
[cs.floatDests, FLOAT_VALUE],
]) {
for (const dest of dests) {
const value = flags[dest];
const candidates = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
for (const part of candidates) {
if (typeof part === 'string' && !pattern.test(part)) {
return usageError(name, node, `error: option '${dest}' expects a numerical value`, style);
}
}
}
}
for (const [dest, allowed] of cs.choicesByDest) {
const value = flags[dest];
const candidates = Array.isArray(value)
? value
: typeof value === 'string' || value instanceof PathSpec
? [value]
: [];
for (const part of candidates) {
const choice = part instanceof PathSpec ? part.virtual : part;
if (!allowed.includes(choice)) {
return usageError(name, node, `error: invalid argument '${choice}' for '${dest}'`, style);
}
}
}
for (const dest of cs.requiredDests) {
if (!(dest in flags) && !(deferEnv && cs.envByDest.has(dest))) {
return usageError(name, node, `error: option '${dest}' is required`, style);
}
}
return null;
}
/**
* Resolve one command line against a CLI tree.
*
* Each level consumes its own options in POSIX order (stop at the first
* non-option word, which names the subcommand), so `git -C <path> status`
* shapes parse the way a terminal user expects. Behavior is pinned to git
* (docker, git 2.47.3): bare group prints its usage to stdout and exits 1,
* `--help` prints the same to stdout and exits 0, an unknown verb refuses
* on stderr with exit 1, and group-level option errors refuse on stderr
* with exit 129. The leaf's own argv is not parsed here; it rides the
* ordinary spec machinery. `head` is the installed head word, used in
* every rendering so a renamed install prints its own name, `cwd` is the
* working directory PATH-typed group values resolve against, so a group option
* resolves the way a leaf option does, and `env` is the session environment,
* so a group option declaring `Argument.env` fills at its own level exactly as
* a leaf one does in the flat parser. `deferEnv` lets target selection reach
* a leaf before fetching managed values; only an absent environment-backed
* required option is deferred, and execution still performs the full walk.
*/
export function walk(head, spec, argv, cwd = '/', env = null, deferEnv = false) {
let node = spec;
// Read once off the root and never off a node: a program answers in one
// voice at every level, so a subcommand cannot pick its own.
const style = spec.usageStyle;
let path = [];
let positions = [];
const flags = {};
const bases = [];
let i = 0;
for (;;) {
// A script node terminates the walk exactly like an fn leaf: its
// remaining argv rides the ordinary spec machinery for validation,
// then passes to the program verbatim.
if (node.subcommands.length === 0) {
return new WalkResult({
leaf: node,
path,
verbPositions: positions,
groupFlags: flags,
operandBases: bases,
argv: argv.slice(i),
});
}
const name = [head, ...path].join(' ');
const cs = compileSpec(node);
let descended = false;
let optionsEnded = false;
while (i < argv.length) {
const token = argv[i];
if (token === undefined)
break;
const alias = !optionsEnded && token.startsWith('-') && !cs.dest.has(token) && token !== '--help'
? findChild(node, token)
: null;
if (alias?.aliases.includes(token)) {
const refused = finishNode(name, node, cs, flags, cwd, style, bases, env, deferEnv);
if (refused !== null)
return refused;
node = alias;
path = [...path, alias.name];
positions = [...positions, i];
i += 1;
descended = true;
break;
}
if (!optionsEnded &&
node.addHelp &&
token === '-h' &&
(style === UsageStyle.ARGPARSE || style === UsageStyle.COBRA) &&
!cs.dest.has('-h')) {
return new WalkResult({
output: encodeText(nodeHelp(name, node, style)),
});
}
if (!optionsEnded && token === '--') {
if (style === UsageStyle.GIT && path.length === 0)
return usageError(name, node, 'unknown option: --', style, token);
optionsEnded = true;
i += 1;
continue;
}
if (!optionsEnded && token.startsWith('--')) {
const eq = token.indexOf('=');
let spelling = eq === -1 ? token : token.slice(0, eq);
const attached = eq === -1 ? null : token.slice(eq + 1);
// getopt_long: an exact spelling wins; otherwise a unique prefix
// expands (git status --porcel) and an ambiguous one is refused
// with every possibility (git wording).
if (node.allowAbbrev && !cs.dest.has(spelling) && spelling !== '--help') {
const candidates = expandGroupLong(node, cs, spelling);
if (candidates.length === 1) {
spelling = candidates[0] ?? spelling;
}
else if (candidates.length > 1) {
const possible = candidates.join(' or ');
return usageError(name, node, `error: ambiguous option: ${spelling.slice(2)} (could be ${possible})`, style);
}
}
// Optional-value longs sit in BOTH longBoolSpellings and
// longOptionalSpellings, so the optional test runs first or
// --color=auto would be refused as taking no value.
if (cs.longOptionalSpellings.has(spelling)) {
if (attached !== null) {
recordValue(flags, cs, spelling, attached);
}
else if (cs.detachedOptionalSpellings.has(spelling) && optionalValue(argv[i + 1])) {
recordValue(flags, cs, spelling, argv[i + 1] ?? '');
i += 1;
}
else {
recordBool(flags, cs, spelling);
}
}
else if (cs.longBoolSpellings.has(spelling)) {
if (attached !== null) {
return usageError(name, node, `error: option '${spelling}' takes no value`, style);
}
recordBool(flags, cs, spelling);
}
else if (cs.longValueSpellings.has(spelling)) {
const next = argv[i + 1];
const width = cs.nargsByDest.get(cs.destOf(spelling));
if (width === 1 && attached !== null) {
recordValues(flags, cs, spelling, [attached]);
}
else if (width !== undefined) {
if (attached !== null)
return usageError(name, node, `unknown option: ${token}`, style, token);
if (argv.length - i - 1 < width)
return usageError(name, node, `error: option '${spelling}' requires a value`, style);
recordValues(flags, cs, spelling, argv.slice(i + 1, i + 1 + width));
i += width;
}
else if (attached !== null) {
recordValue(flags, cs, spelling, attached);
}
else if (next !== undefined) {
i += 1;
recordValue(flags, cs, spelling, next);
}
else {
return usageError(name, node, `error: option '${spelling}' requires a value`, style);
}
}
else if (spelling === '--help' && node.addHelp) {
if (attached !== null) {
return usageError(name, node, `error: option '${spelling}' takes no value`, style);
}
return new WalkResult({
output: encodeText(nodeHelp(name, node, style)),
});
}
else {
return usageError(name, node, `unknown option: ${spelling}`, style, style === UsageStyle.GIT ? token : spelling);
}
i += 1;
continue;
}
if (!optionsEnded && token.startsWith('-') && token !== '-') {
// Declared multi-char shorts (find-style -name) match the whole
// token before any cluster splitting, longest first, the same
// precedence the flat parser uses.
const whole = matchShort(name, node, cs, flags, token, argv.slice(i + 1), style);
if (whole !== null) {
const [consumed, refused] = whole;
if (refused !== null)
return refused;
i += consumed;
continue;
}
let error = null;
let unknown;
let j = 1;
while (j < token.length) {
const spelling = `-${token.charAt(j)}`;
if (cs.detachedOptionalSpellings.has(spelling) ||
(cs.attachSpellings.includes(spelling) && j + 1 < token.length) ||
(!cs.boolSpellings.has(spelling) && cs.dest.has(spelling))) {
const matched = matchShort(name, node, cs, flags, `-${token.slice(j)}`, argv.slice(i + 1), style);
if (matched !== null) {
const [consumed, refused] = matched;
if (refused !== null)
return refused;
i += consumed - 1;
break;
}
error = `unknown option: ${spelling}`;
unknown = spelling;
break;
}
else if (cs.boolSpellings.has(spelling)) {
recordBool(flags, cs, spelling);
j += 1;
}
else {
error = `unknown option: ${spelling}`;
unknown = spelling;
break;
}
}
if (error !== null) {
return usageError(name, node, error, style, unknown);
}
i += 1;
continue;
}
const refused = finishNode(name, node, cs, flags, cwd, style, bases, env, deferEnv);
if (refused !== null)
return refused;
// An alias resolves to its canonical node; the path records the
// canonical name (argparse prog attribution: errors under `gws co`
// render as `gws checkout`).
const child = token.startsWith('-') ? null : findChild(node, token);
if (child === null) {
return unknownVerb(head, name, token);
}
node = child;
path = [...path, child.name];
positions = [...positions, i];
i += 1;
descended = true;
break;
}
if (descended)
continue;
const refused = finishNode(name, node, cs, flags, cwd, style, bases, env, deferEnv);
if (refused !== null)
return refused;
return new WalkResult({
output: encodeText(nodeHelp(name, node, style)),
stream: 'stdout',
exitCode: 1,
});
}
}
//# sourceMappingURL=walk.js.map