import { axiError } from "./errors.js"; export interface FlagSpec { /** Flag names (e.g. "--state") mapped to how they are parsed. */ [name: string]: { takesValue: boolean; /** Accumulate every occurrence into `lists` instead of `flags` (e.g. `--label`). */ repeatable?: boolean; }; } export interface ParsedFlags { /** Single-valued flags: the value, or `true` for a bare switch. */ flags: Record; /** * Values of each repeatable flag in argv order. Every repeatable flag in the * spec is present, holding an empty array when it was not passed. */ lists: Record; positionals: string[]; } export interface SplitFlag { name: string; inlineValue?: string; } /** * Read a value-taking flag. `parseFlags` rejects such a flag without a value, * so anything present here is a string; the `true` case only arises for bare * switches, which callers never read through this helper. */ export function flagValue( flags: Record, name: string, ): string | undefined { const value = flags[name]; return typeof value === "string" ? value : undefined; } /** ["open", "closed", "all"] → "open, closed, or all". */ function orList(values: readonly string[]): string { if (values.length < 2) { return values[0] ?? ""; } return `${values.slice(0, -1).join(", ")}, or ${values[values.length - 1]}`; } /** * Read a flag whose value must be one of a fixed set. Returns undefined when the * flag was absent, leaving the default to the caller — a flag with no default * (`--sort`) and one with a default (`--state`) then differ only in what they do * with that undefined. */ export function parseEnumFlag( value: string | true | undefined, name: string, allowed: readonly T[], suggestions: string[], ): T | undefined { if (value === undefined) { return undefined; } if (value === true || !allowed.includes(value as T)) { throw axiError( `Invalid ${name} value: ${String(value)} (expected ${orList(allowed)})`, "VALIDATION_ERROR", suggestions, ); } return value as T; } /** * Parse a flag's value as a positive integer, rejecting a bare switch or a * non-integer with a uniform `VALIDATION_ERROR`. `label` names the flag in the * message (e.g. "--limit", "--label-id"). Shared by every flag that takes one. */ export function parsePositiveInt( value: string | true, label: string, suggestions: string[] = [], ): number { const parsed = Number(value); if (value === true || !Number.isInteger(parsed) || parsed < 1) { throw axiError( `Invalid ${label} value: ${String(value)} (expected a positive integer)`, "VALIDATION_ERROR", suggestions, ); } return parsed; } /** Split "--flag=value" into name and inline value; "--flag" has none. */ export function splitFlag(arg: string): SplitFlag { const equals = arg.indexOf("="); if (equals === -1) { return { name: arg }; } return { name: arg.slice(0, equals), inlineValue: arg.slice(equals + 1) }; } /** * Resolve a value-taking flag's value from its inline form or the next * argument, returning the index of the last argument consumed. */ export function consumeFlagValue( args: string[], index: number, flag: SplitFlag, suggestions: string[] = [], ): { value: string; lastIndex: number } { if (flag.inlineValue !== undefined) { if (!flag.inlineValue) { throw axiError(`Flag ${flag.name} requires a value`, "VALIDATION_ERROR", suggestions); } return { value: flag.inlineValue, lastIndex: index }; } const next = args[index + 1]; if (next === undefined || next.startsWith("-")) { throw axiError(`Flag ${flag.name} requires a value`, "VALIDATION_ERROR", suggestions); } return { value: next, lastIndex: index + 1 }; } /** "issue" → "an issue"; "pull request" → "a pull request". */ function withArticle(noun: string): string { return /^[aeiou]/i.test(noun) ? `an ${noun}` : `a ${noun}`; } /** * Validate a single raw argument as a positive issue number. `noun` names what * the number identifies ("issue", "target") in the error; `suggestions` carries * the caller's help line. Shared by the single-positional parser and the * two-positional dependency parser so the rule and its message live in one place. */ export function parseIssueNumber(raw: string, noun: string, suggestions: string[]): number { const number = Number(raw); if (!Number.isInteger(number) || number < 1) { throw axiError( `Invalid ${noun} number: ${raw} (expected a positive integer)`, "VALIDATION_ERROR", suggestions, ); } return number; } /** * Take the single positional of a ` ` invocation, rejecting a * missing or extra one. `what` is the noun phrase the arg is ("an issue number", * "a label name") and `placeholder` is how it reads in the usage line * (``, ``); the count checks and their messages are identical for * every single-positional command, number or name. */ export function parseSinglePositional( positionals: string[], command: string, what: string, placeholder: string, ): string { if (positionals.length === 0) { throw axiError(`${command} requires ${what}`, "VALIDATION_ERROR", [ `Run \`gitea-axi ${command} ${placeholder}\``, ]); } if (positionals.length > 1) { throw axiError(`Unexpected argument: ${positionals[1]}`, "VALIDATION_ERROR", [ `Run \`gitea-axi ${command} --help\` to see available flags`, ]); } return positionals[0]!; } /** * Parse the single positional number of a ` ` invocation. * `noun` names what the number identifies ("issue", "pull request") and appears * in the errors; the parsing itself is identical for both. */ export function parsePositionalNumber( positionals: string[], command: string, noun: string, ): number { const raw = parseSinglePositional(positionals, command, `${withArticle(noun)} number`, ""); return parseIssueNumber(raw, noun, [ `Run \`gitea-axi ${command} --help\` to see available flags`, ]); } export function parseFlags( args: string[], spec: FlagSpec, helpCommand: string, ): ParsedFlags { const flags: Record = {}; const lists: Record = {}; for (const [name, entry] of Object.entries(spec)) { if (entry.repeatable) { lists[name] = []; } } const positionals: string[] = []; const helpSuggestion = [`Run \`gitea-axi ${helpCommand} --help\` to see available flags`]; for (let i = 0; i < args.length; i++) { const arg = args[i]!; if (!arg.startsWith("-")) { positionals.push(arg); continue; } const flag = splitFlag(arg); const entry = spec[flag.name]; if (!entry) { throw axiError(`Unknown flag: ${flag.name}`, "VALIDATION_ERROR", helpSuggestion); } if (!entry.takesValue) { if (flag.inlineValue !== undefined) { throw axiError(`Flag ${flag.name} does not take a value`, "VALIDATION_ERROR", helpSuggestion); } flags[flag.name] = true; continue; } const consumed = consumeFlagValue(args, i, flag, helpSuggestion); if (entry.repeatable) { lists[flag.name]!.push(consumed.value); } else { flags[flag.name] = consumed.value; } i = consumed.lastIndex; } return { flags, lists, positionals }; }