basant307/AI_Governance_Project
048
1const { humanReadableArgName } = require('./argument.js');2 3/**4 * TypeScript import types for JSDoc, used by Visual Studio Code IntelliSense and `npm run typescript-checkJS`5 * https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#import-types6 * @typedef { import("./argument.js").Argument } Argument7 * @typedef { import("./command.js").Command } Command8 * @typedef { import("./option.js").Option } Option9 */10 11// Although this is a class, methods are static in style to allow override using subclass or just functions.12class Help {13 constructor() {14 this.helpWidth = undefined;15 this.sortSubcommands = false;16 this.sortOptions = false;17 this.showGlobalOptions = false;18 }19 20 /**21 * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.22 *23 * @param {Command} cmd24 * @returns {Command[]}25 */26 27 visibleCommands(cmd) {28 const visibleCommands = cmd.commands.filter((cmd) => !cmd._hidden);29 const helpCommand = cmd._getHelpCommand();30 if (helpCommand && !helpCommand._hidden) {31 visibleCommands.push(helpCommand);32 }33 if (this.sortSubcommands) {34 visibleCommands.sort((a, b) => {35 // @ts-ignore: because overloaded return type36 return a.name().localeCompare(b.name());37 });38 }39 return visibleCommands;40 }41 42 /**43 * Compare options for sort.44 *45 * @param {Option} a46 * @param {Option} b47 * @returns {number}48 */49 compareOptions(a, b) {50 const getSortKey = (option) => {51 // WYSIWYG for order displayed in help. Short used for comparison if present. No special handling for negated.52 return option.short53 ? option.short.replace(/^-/, '')54 : option.long.replace(/^--/, '');55 };56 return getSortKey(a).localeCompare(getSortKey(b));57 }58 59 /**60 * Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one.61 *62 * @param {Command} cmd63 * @returns {Option[]}64 */65 66 visibleOptions(cmd) {67 const visibleOptions = cmd.options.filter((option) => !option.hidden);68 // Built-in help option.69 const helpOption = cmd._getHelpOption();70 if (helpOption && !helpOption.hidden) {71 // Automatically hide conflicting flags. Bit dubious but a historical behaviour that is convenient for single-command programs.72 const removeShort = helpOption.short && cmd._findOption(helpOption.short);73 const removeLong = helpOption.long && cmd._findOption(helpOption.long);74 if (!removeShort && !removeLong) {75 visibleOptions.push(helpOption); // no changes needed76 } else if (helpOption.long && !removeLong) {77 visibleOptions.push(78 cmd.createOption(helpOption.long, helpOption.description),79 );80 } else if (helpOption.short && !removeShort) {81 visibleOptions.push(82 cmd.createOption(helpOption.short, helpOption.description),83 );84 }85 }86 if (this.sortOptions) {87 visibleOptions.sort(this.compareOptions);88 }89 return visibleOptions;90 }91 92 /**93 * Get an array of the visible global options. (Not including help.)94 *95 * @param {Command} cmd96 * @returns {Option[]}97 */98 99 visibleGlobalOptions(cmd) {100 if (!this.showGlobalOptions) return [];101 102 const globalOptions = [];103 for (104 let ancestorCmd = cmd.parent;105 ancestorCmd;106 ancestorCmd = ancestorCmd.parent107 ) {108 const visibleOptions = ancestorCmd.options.filter(109 (option) => !option.hidden,110 );111 globalOptions.push(...visibleOptions);112 }113 if (this.sortOptions) {114 globalOptions.sort(this.compareOptions);115 }116 return globalOptions;117 }118 119 /**120 * Get an array of the arguments if any have a description.121 *122 * @param {Command} cmd123 * @returns {Argument[]}124 */125 126 visibleArguments(cmd) {127 // Side effect! Apply the legacy descriptions before the arguments are displayed.128 if (cmd._argsDescription) {129 cmd.registeredArguments.forEach((argument) => {130 argument.description =131 argument.description || cmd._argsDescription[argument.name()] || '';132 });133 }134 135 // If there are any arguments with a description then return all the arguments.136 if (cmd.registeredArguments.find((argument) => argument.description)) {137 return cmd.registeredArguments;138 }139 return [];140 }141 142 /**143 * Get the command term to show in the list of subcommands.144 *145 * @param {Command} cmd146 * @returns {string}147 */148 149 subcommandTerm(cmd) {150 // Legacy. Ignores custom usage string, and nested commands.151 const args = cmd.registeredArguments152 .map((arg) => humanReadableArgName(arg))153 .join(' ');154 return (155 cmd._name +156 (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +157 (cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option158 (args ? ' ' + args : '')159 );160 }161 162 /**163 * Get the option term to show in the list of options.164 *165 * @param {Option} option166 * @returns {string}167 */168 169 optionTerm(option) {170 return option.flags;171 }172 173 /**174 * Get the argument term to show in the list of arguments.175 *176 * @param {Argument} argument177 * @returns {string}178 */179 180 argumentTerm(argument) {181 return argument.name();182 }183 184 /**185 * Get the longest command term length.186 *187 * @param {Command} cmd188 * @param {Help} helper189 * @returns {number}190 */191 192 longestSubcommandTermLength(cmd, helper) {193 return helper.visibleCommands(cmd).reduce((max, command) => {194 return Math.max(max, helper.subcommandTerm(command).length);195 }, 0);196 }197 198 /**199 * Get the longest option term length.200 *201 * @param {Command} cmd202 * @param {Help} helper203 * @returns {number}204 */205 206 longestOptionTermLength(cmd, helper) {207 return helper.visibleOptions(cmd).reduce((max, option) => {208 return Math.max(max, helper.optionTerm(option).length);209 }, 0);210 }211 212 /**213 * Get the longest global option term length.214 *215 * @param {Command} cmd216 * @param {Help} helper217 * @returns {number}218 */219 220 longestGlobalOptionTermLength(cmd, helper) {221 return helper.visibleGlobalOptions(cmd).reduce((max, option) => {222 return Math.max(max, helper.optionTerm(option).length);223 }, 0);224 }225 226 /**227 * Get the longest argument term length.228 *229 * @param {Command} cmd230 * @param {Help} helper231 * @returns {number}232 */233 234 longestArgumentTermLength(cmd, helper) {235 return helper.visibleArguments(cmd).reduce((max, argument) => {236 return Math.max(max, helper.argumentTerm(argument).length);237 }, 0);238 }239 240 /**241 * Get the command usage to be displayed at the top of the built-in help.242 *243 * @param {Command} cmd244 * @returns {string}245 */246 247 commandUsage(cmd) {248 // Usage249 let cmdName = cmd._name;250 if (cmd._aliases[0]) {251 cmdName = cmdName + '|' + cmd._aliases[0];252 }253 let ancestorCmdNames = '';254 for (255 let ancestorCmd = cmd.parent;256 ancestorCmd;257 ancestorCmd = ancestorCmd.parent258 ) {259 ancestorCmdNames = ancestorCmd.name() + ' ' + ancestorCmdNames;260 }261 return ancestorCmdNames + cmdName + ' ' + cmd.usage();262 }263 264 /**265 * Get the description for the command.266 *267 * @param {Command} cmd268 * @returns {string}269 */270 271 commandDescription(cmd) {272 // @ts-ignore: because overloaded return type273 return cmd.description();274 }275 276 /**277 * Get the subcommand summary to show in the list of subcommands.278 * (Fallback to description for backwards compatibility.)279 *280 * @param {Command} cmd281 * @returns {string}282 */283 284 subcommandDescription(cmd) {285 // @ts-ignore: because overloaded return type286 return cmd.summary() || cmd.description();287 }288 289 /**290 * Get the option description to show in the list of options.291 *292 * @param {Option} option293 * @return {string}294 */295 296 optionDescription(option) {297 const extraInfo = [];298 299 if (option.argChoices) {300 extraInfo.push(301 // use stringify to match the display of the default value302 `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,303 );304 }305 if (option.defaultValue !== undefined) {306 // default for boolean and negated more for programmer than end user,307 // but show true/false for boolean option as may be for hand-rolled env or config processing.308 const showDefault =309 option.required ||310 option.optional ||311 (option.isBoolean() && typeof option.defaultValue === 'boolean');312 if (showDefault) {313 extraInfo.push(314 `default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`,315 );316 }317 }318 // preset for boolean and negated are more for programmer than end user319 if (option.presetArg !== undefined && option.optional) {320 extraInfo.push(`preset: ${JSON.stringify(option.presetArg)}`);321 }322 if (option.envVar !== undefined) {323 extraInfo.push(`env: ${option.envVar}`);324 }325 if (extraInfo.length > 0) {326 return `${option.description} (${extraInfo.join(', ')})`;327 }328 329 return option.description;330 }331 332 /**333 * Get the argument description to show in the list of arguments.334 *335 * @param {Argument} argument336 * @return {string}337 */338 339 argumentDescription(argument) {340 const extraInfo = [];341 if (argument.argChoices) {342 extraInfo.push(343 // use stringify to match the display of the default value344 `choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,345 );346 }347 if (argument.defaultValue !== undefined) {348 extraInfo.push(349 `default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`,350 );351 }352 if (extraInfo.length > 0) {353 const extraDescripton = `(${extraInfo.join(', ')})`;354 if (argument.description) {355 return `${argument.description} ${extraDescripton}`;356 }357 return extraDescripton;358 }359 return argument.description;360 }361 362 /**363 * Generate the built-in help text.364 *365 * @param {Command} cmd366 * @param {Help} helper367 * @returns {string}368 */369 370 formatHelp(cmd, helper) {371 const termWidth = helper.padWidth(cmd, helper);372 const helpWidth = helper.helpWidth || 80;373 const itemIndentWidth = 2;374 const itemSeparatorWidth = 2; // between term and description375 function formatItem(term, description) {376 if (description) {377 const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;378 return helper.wrap(379 fullText,380 helpWidth - itemIndentWidth,381 termWidth + itemSeparatorWidth,382 );383 }384 return term;385 }386 function formatList(textArray) {387 return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));388 }389 390 // Usage391 let output = [`Usage: ${helper.commandUsage(cmd)}`, ''];392 393 // Description394 const commandDescription = helper.commandDescription(cmd);395 if (commandDescription.length > 0) {396 output = output.concat([397 helper.wrap(commandDescription, helpWidth, 0),398 '',399 ]);400 }401 402 // Arguments403 const argumentList = helper.visibleArguments(cmd).map((argument) => {404 return formatItem(405 helper.argumentTerm(argument),406 helper.argumentDescription(argument),407 );408 });409 if (argumentList.length > 0) {410 output = output.concat(['Arguments:', formatList(argumentList), '']);411 }412 413 // Options414 const optionList = helper.visibleOptions(cmd).map((option) => {415 return formatItem(416 helper.optionTerm(option),417 helper.optionDescription(option),418 );419 });420 if (optionList.length > 0) {421 output = output.concat(['Options:', formatList(optionList), '']);422 }423 424 if (this.showGlobalOptions) {425 const globalOptionList = helper426 .visibleGlobalOptions(cmd)427 .map((option) => {428 return formatItem(429 helper.optionTerm(option),430 helper.optionDescription(option),431 );432 });433 if (globalOptionList.length > 0) {434 output = output.concat([435 'Global Options:',436 formatList(globalOptionList),437 '',438 ]);439 }440 }441 442 // Commands443 const commandList = helper.visibleCommands(cmd).map((cmd) => {444 return formatItem(445 helper.subcommandTerm(cmd),446 helper.subcommandDescription(cmd),447 );448 });449 if (commandList.length > 0) {450 output = output.concat(['Commands:', formatList(commandList), '']);451 }452 453 return output.join('\n');454 }455 456 /**457 * Calculate the pad width from the maximum term length.458 *459 * @param {Command} cmd460 * @param {Help} helper461 * @returns {number}462 */463 464 padWidth(cmd, helper) {465 return Math.max(466 helper.longestOptionTermLength(cmd, helper),467 helper.longestGlobalOptionTermLength(cmd, helper),468 helper.longestSubcommandTermLength(cmd, helper),469 helper.longestArgumentTermLength(cmd, helper),470 );471 }472 473 /**474 * Wrap the given string to width characters per line, with lines after the first indented.475 * Do not wrap if insufficient room for wrapping (minColumnWidth), or string is manually formatted.476 *477 * @param {string} str478 * @param {number} width479 * @param {number} indent480 * @param {number} [minColumnWidth=40]481 * @return {string}482 *483 */484 485 wrap(str, width, indent, minColumnWidth = 40) {486 // Full \s characters, minus the linefeeds.487 const indents =488 ' \\f\\t\\v\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff';489 // Detect manually wrapped and indented strings by searching for line break followed by spaces.490 const manualIndent = new RegExp(`[\\n][${indents}]+`);491 if (str.match(manualIndent)) return str;492 // Do not wrap if not enough room for a wrapped column of text (as could end up with a word per line).493 const columnWidth = width - indent;494 if (columnWidth < minColumnWidth) return str;495 496 const leadingStr = str.slice(0, indent);497 const columnText = str.slice(indent).replace('\r\n', '\n');498 const indentString = ' '.repeat(indent);499 const zeroWidthSpace = '\u200B';500 const breaks = `\\s${zeroWidthSpace}`;501 // Match line end (so empty lines don't collapse),502 // or as much text as will fit in column, or excess text up to first break.503 const regex = new RegExp(504 `\n|.{1,${columnWidth - 1}}([${breaks}]|$)|[^${breaks}]+?([${breaks}]|$)`,505 'g',506 );507 const lines = columnText.match(regex) || [];508 return (509 leadingStr +510 lines511 .map((line, i) => {512 if (line === '\n') return ''; // preserve empty lines513 return (i > 0 ? indentString : '') + line.trimEnd();514 })515 .join('\n')516 );517 }518}519 520exports.Help = Help;521 