commander 12.1.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -79,7 +79,8 @@ const { program } = require('commander');
79
79
 
80
80
  program
81
81
  .option('--first')
82
- .option('-s, --separator <char>');
82
+ .option('-s, --separator <char>')
83
+ .argument('<string>');
83
84
 
84
85
  program.parse();
85
86
 
@@ -678,8 +679,7 @@ async function main() {
678
679
  }
679
680
  ```
680
681
 
681
- A command's options and arguments on the command line are validated when the command is used. Any unknown options or missing arguments will be reported as an error. You can suppress the unknown option checks with `.allowUnknownOption()`. By default, it is not an error to
682
- pass more arguments than declared, but you can make this an error with `.allowExcessArguments(false)`.
682
+ A command's options and arguments on the command line are validated when the command is used. Any unknown options or missing arguments or excess arguments will be reported as an error. You can suppress the unknown option check with `.allowUnknownOption()`. You can suppress the excess arguments check with `.allowExcessArguments()`.
683
683
 
684
684
  ### Stand-alone executable (sub)commands
685
685
 
@@ -696,7 +696,7 @@ Example file: [pm](./examples/pm)
696
696
  program
697
697
  .name('pm')
698
698
  .version('0.1.0')
699
- .command('install [name]', 'install one or more packages')
699
+ .command('install [package-names...]', 'install one or more packages')
700
700
  .command('search [query]', 'search with optional query')
701
701
  .command('update', 'update installed packages', { executableFile: 'myUpdateSubCommand' })
702
702
  .command('list', 'list packages installed', { isDefault: true });
@@ -921,25 +921,11 @@ program.helpCommand('assist [command]', 'show assistance');
921
921
  ### More configuration
922
922
 
923
923
  The built-in help is formatted using the Help class.
924
- You can configure the Help behaviour by modifying data properties and methods using `.configureHelp()`, or by subclassing using `.createHelp()` if you prefer.
924
+ You can configure the help by modifying data properties and methods using `.configureHelp()`, or by subclassing Help using `.createHelp()` .
925
925
 
926
- The data properties are:
926
+ Simple properties include `sortSubcommands`, `sortOptions`, and `showGlobalOptions`. You can add color using the style methods like `styleTitle()`.
927
927
 
928
- - `helpWidth`: specify the wrap width, useful for unit tests
929
- - `sortSubcommands`: sort the subcommands alphabetically
930
- - `sortOptions`: sort the options alphabetically
931
- - `showGlobalOptions`: show a section with the global options from the parent command(s)
932
-
933
- You can override any method on the [Help](./lib/help.js) class. There are methods getting the visible lists of arguments, options, and subcommands. There are methods for formatting the items in the lists, with each item having a _term_ and _description_. Take a look at `.formatHelp()` to see how they are used.
934
-
935
- Example file: [configure-help.js](./examples/configure-help.js)
936
-
937
- ```js
938
- program.configureHelp({
939
- sortSubcommands: true,
940
- subcommandTerm: (cmd) => cmd.name() // Just show the name, instead of short usage.
941
- });
942
- ```
928
+ For more detail and examples of changing the displayed text, color, and layout see (./docs/help-in-depth.md)
943
929
 
944
930
  ## Custom event listeners
945
931
 
@@ -973,8 +959,6 @@ program.parse(['--port', '80'], { from: 'user' }); // just user supplied argumen
973
959
 
974
960
  Use parseAsync instead of parse if any of your action handlers are async.
975
961
 
976
- If you want to parse multiple times, create a new program each time. Calling parse does not clear out any previous state.
977
-
978
962
  ### Parsing Configuration
979
963
 
980
964
  If the default parsing does not suit your needs, there are some behaviours to support other usage patterns.
package/lib/command.js CHANGED
@@ -6,7 +6,7 @@ const process = require('node:process');
6
6
 
7
7
  const { Argument, humanReadableArgName } = require('./argument.js');
8
8
  const { CommanderError } = require('./error.js');
9
- const { Help } = require('./help.js');
9
+ const { Help, stripColor } = require('./help.js');
10
10
  const { Option, DualOptions } = require('./option.js');
11
11
  const { suggestSimilar } = require('./suggestSimilar');
12
12
 
@@ -25,7 +25,7 @@ class Command extends EventEmitter {
25
25
  this.options = [];
26
26
  this.parent = null;
27
27
  this._allowUnknownOption = false;
28
- this._allowExcessArguments = true;
28
+ this._allowExcessArguments = false;
29
29
  /** @type {Argument[]} */
30
30
  this.registeredArguments = [];
31
31
  this._args = this.registeredArguments; // deprecated old name
@@ -55,16 +55,22 @@ class Command extends EventEmitter {
55
55
  /** @type {(boolean | string)} */
56
56
  this._showHelpAfterError = false;
57
57
  this._showSuggestionAfterError = true;
58
+ this._savedState = null; // used in save/restoreStateBeforeParse
58
59
 
59
- // see .configureOutput() for docs
60
+ // see configureOutput() for docs
60
61
  this._outputConfiguration = {
61
62
  writeOut: (str) => process.stdout.write(str),
62
63
  writeErr: (str) => process.stderr.write(str),
64
+ outputError: (str, write) => write(str),
63
65
  getOutHelpWidth: () =>
64
66
  process.stdout.isTTY ? process.stdout.columns : undefined,
65
67
  getErrHelpWidth: () =>
66
68
  process.stderr.isTTY ? process.stderr.columns : undefined,
67
- outputError: (str, write) => write(str),
69
+ getOutHasColors: () =>
70
+ useColor() ?? (process.stdout.isTTY && process.stdout.hasColors?.()),
71
+ getErrHasColors: () =>
72
+ useColor() ?? (process.stderr.isTTY && process.stderr.hasColors?.()),
73
+ stripColor: (str) => stripColor(str),
68
74
  };
69
75
 
70
76
  this._hidden = false;
@@ -213,14 +219,18 @@ class Command extends EventEmitter {
213
219
  *
214
220
  * The configuration properties are all functions:
215
221
  *
216
- * // functions to change where being written, stdout and stderr
222
+ * // change how output being written, defaults to stdout and stderr
217
223
  * writeOut(str)
218
224
  * writeErr(str)
219
- * // matching functions to specify width for wrapping help
225
+ * // change how output being written for errors, defaults to writeErr
226
+ * outputError(str, write) // used for displaying errors and not used for displaying help
227
+ * // specify width for wrapping help
220
228
  * getOutHelpWidth()
221
229
  * getErrHelpWidth()
222
- * // functions based on what is being written out
223
- * outputError(str, write) // used for displaying errors, and not used for displaying help
230
+ * // color support, currently only used with Help
231
+ * getOutHasColors()
232
+ * getErrHasColors()
233
+ * stripColor() // used to remove ANSI escape codes if output does not have colors
224
234
  *
225
235
  * @param {object} [configuration] - configuration options
226
236
  * @return {(Command | object)} `this` command for chaining, or stored configuration
@@ -1060,6 +1070,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1060
1070
  */
1061
1071
 
1062
1072
  parse(argv, parseOptions) {
1073
+ this._prepareForParse();
1063
1074
  const userArgs = this._prepareUserArgs(argv, parseOptions);
1064
1075
  this._parseCommand([], userArgs);
1065
1076
 
@@ -1088,12 +1099,82 @@ Expecting one of '${allowedValues.join("', '")}'`);
1088
1099
  */
1089
1100
 
1090
1101
  async parseAsync(argv, parseOptions) {
1102
+ this._prepareForParse();
1091
1103
  const userArgs = this._prepareUserArgs(argv, parseOptions);
1092
1104
  await this._parseCommand([], userArgs);
1093
1105
 
1094
1106
  return this;
1095
1107
  }
1096
1108
 
1109
+ _prepareForParse() {
1110
+ if (this._savedState === null) {
1111
+ this.saveStateBeforeParse();
1112
+ } else {
1113
+ this.restoreStateBeforeParse();
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * Called the first time parse is called to save state and allow a restore before subsequent calls to parse.
1119
+ * Not usually called directly, but available for subclasses to save their custom state.
1120
+ *
1121
+ * This is called in a lazy way. Only commands used in parsing chain will have state saved.
1122
+ */
1123
+ saveStateBeforeParse() {
1124
+ this._savedState = {
1125
+ // name is stable if supplied by author, but may be unspecified for root command and deduced during parsing
1126
+ _name: this._name,
1127
+ // option values before parse have default values (including false for negated options)
1128
+ // shallow clones
1129
+ _optionValues: { ...this._optionValues },
1130
+ _optionValueSources: { ...this._optionValueSources },
1131
+ };
1132
+ }
1133
+
1134
+ /**
1135
+ * Restore state before parse for calls after the first.
1136
+ * Not usually called directly, but available for subclasses to save their custom state.
1137
+ *
1138
+ * This is called in a lazy way. Only commands used in parsing chain will have state restored.
1139
+ */
1140
+ restoreStateBeforeParse() {
1141
+ if (this._storeOptionsAsProperties)
1142
+ throw new Error(`Can not call parse again when storeOptionsAsProperties is true.
1143
+ - either make a new Command for each call to parse, or stop storing options as properties`);
1144
+
1145
+ // clear state from _prepareUserArgs
1146
+ this._name = this._savedState._name;
1147
+ this._scriptPath = null;
1148
+ this.rawArgs = [];
1149
+ // clear state from setOptionValueWithSource
1150
+ this._optionValues = { ...this._savedState._optionValues };
1151
+ this._optionValueSources = { ...this._savedState._optionValueSources };
1152
+ // clear state from _parseCommand
1153
+ this.args = [];
1154
+ // clear state from _processArguments
1155
+ this.processedArgs = [];
1156
+ }
1157
+
1158
+ /**
1159
+ * Throw if expected executable is missing. Add lots of help for author.
1160
+ *
1161
+ * @param {string} executableFile
1162
+ * @param {string} executableDir
1163
+ * @param {string} subcommandName
1164
+ */
1165
+ _checkForMissingExecutable(executableFile, executableDir, subcommandName) {
1166
+ if (fs.existsSync(executableFile)) return;
1167
+
1168
+ const executableDirMessage = executableDir
1169
+ ? `searched for local subcommand relative to directory '${executableDir}'`
1170
+ : 'no directory for search for local subcommand, use .executableDir() to supply a custom directory';
1171
+ const executableMissing = `'${executableFile}' does not exist
1172
+ - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
1173
+ - if the default executable name is not suitable, use the executableFile option to supply a custom name or path
1174
+ - ${executableDirMessage}`;
1175
+ throw new Error(executableMissing);
1176
+ }
1177
+
1097
1178
  /**
1098
1179
  * Execute a sub-command executable.
1099
1180
  *
@@ -1134,7 +1215,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1134
1215
  let resolvedScriptPath; // resolve possible symlink for installed npm binary
1135
1216
  try {
1136
1217
  resolvedScriptPath = fs.realpathSync(this._scriptPath);
1137
- } catch (err) {
1218
+ } catch {
1138
1219
  resolvedScriptPath = this._scriptPath;
1139
1220
  }
1140
1221
  executableDir = path.resolve(
@@ -1177,6 +1258,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1177
1258
  proc = childProcess.spawn(executableFile, args, { stdio: 'inherit' });
1178
1259
  }
1179
1260
  } else {
1261
+ this._checkForMissingExecutable(
1262
+ executableFile,
1263
+ executableDir,
1264
+ subcommand._name,
1265
+ );
1180
1266
  args.unshift(executableFile);
1181
1267
  // add executable arguments to spawn
1182
1268
  args = incrementNodeInspectorPort(process.execArgv).concat(args);
@@ -1215,14 +1301,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1215
1301
  proc.on('error', (err) => {
1216
1302
  // @ts-ignore: because err.code is an unknown property
1217
1303
  if (err.code === 'ENOENT') {
1218
- const executableDirMessage = executableDir
1219
- ? `searched for local subcommand relative to directory '${executableDir}'`
1220
- : 'no directory for search for local subcommand, use .executableDir() to supply a custom directory';
1221
- const executableMissing = `'${executableFile}' does not exist
1222
- - if '${subcommand._name}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
1223
- - if the default executable name is not suitable, use the executableFile option to supply a custom name or path
1224
- - ${executableDirMessage}`;
1225
- throw new Error(executableMissing);
1304
+ this._checkForMissingExecutable(
1305
+ executableFile,
1306
+ executableDir,
1307
+ subcommand._name,
1308
+ );
1226
1309
  // @ts-ignore: because err.code is an unknown property
1227
1310
  } else if (err.code === 'EACCES') {
1228
1311
  throw new Error(`'${executableFile}' not executable`);
@@ -1252,6 +1335,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1252
1335
  const subCommand = this._findCommand(commandName);
1253
1336
  if (!subCommand) this.help({ error: true });
1254
1337
 
1338
+ subCommand._prepareForParse();
1255
1339
  let promiseChain;
1256
1340
  promiseChain = this._chainOrCallSubCommandHook(
1257
1341
  promiseChain,
@@ -1629,6 +1713,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
1629
1713
  * Parse options from `argv` removing known options,
1630
1714
  * and return argv split into operands and unknown arguments.
1631
1715
  *
1716
+ * Side effects: modifies command by storing options. Does not reset state if called again.
1717
+ *
1632
1718
  * Examples:
1633
1719
  *
1634
1720
  * argv => operands, unknown
@@ -2254,31 +2340,49 @@ Expecting one of '${allowedValues.join("', '")}'`);
2254
2340
 
2255
2341
  helpInformation(contextOptions) {
2256
2342
  const helper = this.createHelp();
2257
- if (helper.helpWidth === undefined) {
2258
- helper.helpWidth =
2259
- contextOptions && contextOptions.error
2260
- ? this._outputConfiguration.getErrHelpWidth()
2261
- : this._outputConfiguration.getOutHelpWidth();
2262
- }
2263
- return helper.formatHelp(this, helper);
2343
+ const context = this._getOutputContext(contextOptions);
2344
+ helper.prepareContext({
2345
+ error: context.error,
2346
+ helpWidth: context.helpWidth,
2347
+ outputHasColors: context.hasColors,
2348
+ });
2349
+ const text = helper.formatHelp(this, helper);
2350
+ if (context.hasColors) return text;
2351
+ return this._outputConfiguration.stripColor(text);
2264
2352
  }
2265
2353
 
2266
2354
  /**
2355
+ * @typedef HelpContext
2356
+ * @type {object}
2357
+ * @property {boolean} error
2358
+ * @property {number} helpWidth
2359
+ * @property {boolean} hasColors
2360
+ * @property {function} write - includes stripColor if needed
2361
+ *
2362
+ * @returns {HelpContext}
2267
2363
  * @private
2268
2364
  */
2269
2365
 
2270
- _getHelpContext(contextOptions) {
2366
+ _getOutputContext(contextOptions) {
2271
2367
  contextOptions = contextOptions || {};
2272
- const context = { error: !!contextOptions.error };
2273
- let write;
2274
- if (context.error) {
2275
- write = (arg) => this._outputConfiguration.writeErr(arg);
2368
+ const error = !!contextOptions.error;
2369
+ let baseWrite;
2370
+ let hasColors;
2371
+ let helpWidth;
2372
+ if (error) {
2373
+ baseWrite = (str) => this._outputConfiguration.writeErr(str);
2374
+ hasColors = this._outputConfiguration.getErrHasColors();
2375
+ helpWidth = this._outputConfiguration.getErrHelpWidth();
2276
2376
  } else {
2277
- write = (arg) => this._outputConfiguration.writeOut(arg);
2377
+ baseWrite = (str) => this._outputConfiguration.writeOut(str);
2378
+ hasColors = this._outputConfiguration.getOutHasColors();
2379
+ helpWidth = this._outputConfiguration.getOutHelpWidth();
2278
2380
  }
2279
- context.write = contextOptions.write || write;
2280
- context.command = this;
2281
- return context;
2381
+ const write = (str) => {
2382
+ if (!hasColors) str = this._outputConfiguration.stripColor(str);
2383
+ return baseWrite(str);
2384
+ };
2385
+ return { error, write, hasColors, helpWidth };
2282
2386
  }
2283
2387
 
2284
2388
  /**
@@ -2295,14 +2399,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
2295
2399
  deprecatedCallback = contextOptions;
2296
2400
  contextOptions = undefined;
2297
2401
  }
2298
- const context = this._getHelpContext(contextOptions);
2402
+
2403
+ const outputContext = this._getOutputContext(contextOptions);
2404
+ /** @type {HelpTextEventContext} */
2405
+ const eventContext = {
2406
+ error: outputContext.error,
2407
+ write: outputContext.write,
2408
+ command: this,
2409
+ };
2299
2410
 
2300
2411
  this._getCommandAndAncestors()
2301
2412
  .reverse()
2302
- .forEach((command) => command.emit('beforeAllHelp', context));
2303
- this.emit('beforeHelp', context);
2413
+ .forEach((command) => command.emit('beforeAllHelp', eventContext));
2414
+ this.emit('beforeHelp', eventContext);
2304
2415
 
2305
- let helpInformation = this.helpInformation(context);
2416
+ let helpInformation = this.helpInformation({ error: outputContext.error });
2306
2417
  if (deprecatedCallback) {
2307
2418
  helpInformation = deprecatedCallback(helpInformation);
2308
2419
  if (
@@ -2312,14 +2423,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
2312
2423
  throw new Error('outputHelp callback must return a string or a Buffer');
2313
2424
  }
2314
2425
  }
2315
- context.write(helpInformation);
2426
+ outputContext.write(helpInformation);
2316
2427
 
2317
2428
  if (this._getHelpOption()?.long) {
2318
2429
  this.emit(this._getHelpOption().long); // deprecated
2319
2430
  }
2320
- this.emit('afterHelp', context);
2431
+ this.emit('afterHelp', eventContext);
2321
2432
  this._getCommandAndAncestors().forEach((command) =>
2322
- command.emit('afterAllHelp', context),
2433
+ command.emit('afterAllHelp', eventContext),
2323
2434
  );
2324
2435
  }
2325
2436
 
@@ -2339,6 +2450,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
2339
2450
  helpOption(flags, description) {
2340
2451
  // Support disabling built-in help option.
2341
2452
  if (typeof flags === 'boolean') {
2453
+ // true is not an expected value. Do something sensible but no unit-test.
2454
+ // istanbul ignore if
2342
2455
  if (flags) {
2343
2456
  this._helpOption = this._helpOption ?? undefined; // preserve existing option
2344
2457
  } else {
@@ -2392,7 +2505,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2392
2505
 
2393
2506
  help(contextOptions) {
2394
2507
  this.outputHelp(contextOptions);
2395
- let exitCode = process.exitCode || 0;
2508
+ let exitCode = Number(process.exitCode ?? 0); // process.exitCode does allow a string or an integer, but we prefer just a number
2396
2509
  if (
2397
2510
  exitCode === 0 &&
2398
2511
  contextOptions &&
@@ -2405,6 +2518,15 @@ Expecting one of '${allowedValues.join("', '")}'`);
2405
2518
  this._exit(exitCode, 'commander.help', '(outputHelp)');
2406
2519
  }
2407
2520
 
2521
+ /**
2522
+ * // Do a little typing to coordinate emit and listener for the help text events.
2523
+ * @typedef HelpTextEventContext
2524
+ * @type {object}
2525
+ * @property {boolean} error
2526
+ * @property {Command} command
2527
+ * @property {function} write
2528
+ */
2529
+
2408
2530
  /**
2409
2531
  * Add additional text to be displayed with the built-in help.
2410
2532
  *
@@ -2415,14 +2537,16 @@ Expecting one of '${allowedValues.join("', '")}'`);
2415
2537
  * @param {(string | Function)} text - string to add, or a function returning a string
2416
2538
  * @return {Command} `this` command for chaining
2417
2539
  */
2540
+
2418
2541
  addHelpText(position, text) {
2419
2542
  const allowedValues = ['beforeAll', 'before', 'after', 'afterAll'];
2420
2543
  if (!allowedValues.includes(position)) {
2421
2544
  throw new Error(`Unexpected value for position to addHelpText.
2422
2545
  Expecting one of '${allowedValues.join("', '")}'`);
2423
2546
  }
2547
+
2424
2548
  const helpEvent = `${position}Help`;
2425
- this.on(helpEvent, (context) => {
2549
+ this.on(helpEvent, (/** @type {HelpTextEventContext} */ context) => {
2426
2550
  let helpStr;
2427
2551
  if (typeof text === 'function') {
2428
2552
  helpStr = text({ error: context.error, command: context.command });
@@ -2506,4 +2630,33 @@ function incrementNodeInspectorPort(args) {
2506
2630
  });
2507
2631
  }
2508
2632
 
2633
+ /**
2634
+ * @returns {boolean | undefined}
2635
+ * @package
2636
+ */
2637
+ function useColor() {
2638
+ // Test for common conventions.
2639
+ // NB: the observed behaviour is in combination with how author adds color! For example:
2640
+ // - we do not test NODE_DISABLE_COLORS, but util:styletext does
2641
+ // - we do test NO_COLOR, but Chalk does not
2642
+ //
2643
+ // References:
2644
+ // https://no-color.org
2645
+ // https://bixense.com/clicolors/
2646
+ // https://github.com/nodejs/node/blob/0a00217a5f67ef4a22384cfc80eb6dd9a917fdc1/lib/internal/tty.js#L109
2647
+ // https://github.com/chalk/supports-color/blob/c214314a14bcb174b12b3014b2b0a8de375029ae/index.js#L33
2648
+ // (https://force-color.org recent web page from 2023, does not match major javascript implementations)
2649
+
2650
+ if (
2651
+ process.env.NO_COLOR ||
2652
+ process.env.FORCE_COLOR === '0' ||
2653
+ process.env.FORCE_COLOR === 'false'
2654
+ )
2655
+ return false;
2656
+ if (process.env.FORCE_COLOR || process.env.CLICOLOR_FORCE !== undefined)
2657
+ return true;
2658
+ return undefined;
2659
+ }
2660
+
2509
2661
  exports.Command = Command;
2662
+ exports.useColor = useColor; // exporting for tests
package/lib/help.js CHANGED
@@ -12,11 +12,24 @@ const { humanReadableArgName } = require('./argument.js');
12
12
  class Help {
13
13
  constructor() {
14
14
  this.helpWidth = undefined;
15
+ this.minWidthToWrap = 40;
15
16
  this.sortSubcommands = false;
16
17
  this.sortOptions = false;
17
18
  this.showGlobalOptions = false;
18
19
  }
19
20
 
21
+ /**
22
+ * prepareContext is called by Commander after applying overrides from `Command.configureHelp()`
23
+ * and just before calling `formatHelp()`.
24
+ *
25
+ * Commander just uses the helpWidth and the rest is provided for optional use by more complex subclasses.
26
+ *
27
+ * @param {{ error?: boolean, helpWidth?: number, outputHasColors?: boolean }} contextOptions
28
+ */
29
+ prepareContext(contextOptions) {
30
+ this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
31
+ }
32
+
20
33
  /**
21
34
  * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.
22
35
  *
@@ -191,7 +204,12 @@ class Help {
191
204
 
192
205
  longestSubcommandTermLength(cmd, helper) {
193
206
  return helper.visibleCommands(cmd).reduce((max, command) => {
194
- return Math.max(max, helper.subcommandTerm(command).length);
207
+ return Math.max(
208
+ max,
209
+ this.displayWidth(
210
+ helper.styleSubcommandTerm(helper.subcommandTerm(command)),
211
+ ),
212
+ );
195
213
  }, 0);
196
214
  }
197
215
 
@@ -205,7 +223,10 @@ class Help {
205
223
 
206
224
  longestOptionTermLength(cmd, helper) {
207
225
  return helper.visibleOptions(cmd).reduce((max, option) => {
208
- return Math.max(max, helper.optionTerm(option).length);
226
+ return Math.max(
227
+ max,
228
+ this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
229
+ );
209
230
  }, 0);
210
231
  }
211
232
 
@@ -219,7 +240,10 @@ class Help {
219
240
 
220
241
  longestGlobalOptionTermLength(cmd, helper) {
221
242
  return helper.visibleGlobalOptions(cmd).reduce((max, option) => {
222
- return Math.max(max, helper.optionTerm(option).length);
243
+ return Math.max(
244
+ max,
245
+ this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
246
+ );
223
247
  }, 0);
224
248
  }
225
249
 
@@ -233,7 +257,12 @@ class Help {
233
257
 
234
258
  longestArgumentTermLength(cmd, helper) {
235
259
  return helper.visibleArguments(cmd).reduce((max, argument) => {
236
- return Math.max(max, helper.argumentTerm(argument).length);
260
+ return Math.max(
261
+ max,
262
+ this.displayWidth(
263
+ helper.styleArgumentTerm(helper.argumentTerm(argument)),
264
+ ),
265
+ );
237
266
  }, 0);
238
267
  }
239
268
 
@@ -350,11 +379,11 @@ class Help {
350
379
  );
351
380
  }
352
381
  if (extraInfo.length > 0) {
353
- const extraDescripton = `(${extraInfo.join(', ')})`;
382
+ const extraDescription = `(${extraInfo.join(', ')})`;
354
383
  if (argument.description) {
355
- return `${argument.description} ${extraDescripton}`;
384
+ return `${argument.description} ${extraDescription}`;
356
385
  }
357
- return extraDescripton;
386
+ return extraDescription;
358
387
  }
359
388
  return argument.description;
360
389
  }
@@ -369,71 +398,73 @@ class Help {
369
398
 
370
399
  formatHelp(cmd, helper) {
371
400
  const termWidth = helper.padWidth(cmd, helper);
372
- const helpWidth = helper.helpWidth || 80;
373
- const itemIndentWidth = 2;
374
- const itemSeparatorWidth = 2; // between term and description
375
- 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));
401
+ const helpWidth = helper.helpWidth ?? 80; // in case prepareContext() was not called
402
+
403
+ function callFormatItem(term, description) {
404
+ return helper.formatItem(term, termWidth, description, helper);
388
405
  }
389
406
 
390
407
  // Usage
391
- let output = [`Usage: ${helper.commandUsage(cmd)}`, ''];
408
+ let output = [
409
+ `${helper.styleTitle('Usage:')} ${helper.styleUsage(helper.commandUsage(cmd))}`,
410
+ '',
411
+ ];
392
412
 
393
413
  // Description
394
414
  const commandDescription = helper.commandDescription(cmd);
395
415
  if (commandDescription.length > 0) {
396
416
  output = output.concat([
397
- helper.wrap(commandDescription, helpWidth, 0),
417
+ helper.boxWrap(
418
+ helper.styleCommandDescription(commandDescription),
419
+ helpWidth,
420
+ ),
398
421
  '',
399
422
  ]);
400
423
  }
401
424
 
402
425
  // Arguments
403
426
  const argumentList = helper.visibleArguments(cmd).map((argument) => {
404
- return formatItem(
405
- helper.argumentTerm(argument),
406
- helper.argumentDescription(argument),
427
+ return callFormatItem(
428
+ helper.styleArgumentTerm(helper.argumentTerm(argument)),
429
+ helper.styleArgumentDescription(helper.argumentDescription(argument)),
407
430
  );
408
431
  });
409
432
  if (argumentList.length > 0) {
410
- output = output.concat(['Arguments:', formatList(argumentList), '']);
433
+ output = output.concat([
434
+ helper.styleTitle('Arguments:'),
435
+ ...argumentList,
436
+ '',
437
+ ]);
411
438
  }
412
439
 
413
440
  // Options
414
441
  const optionList = helper.visibleOptions(cmd).map((option) => {
415
- return formatItem(
416
- helper.optionTerm(option),
417
- helper.optionDescription(option),
442
+ return callFormatItem(
443
+ helper.styleOptionTerm(helper.optionTerm(option)),
444
+ helper.styleOptionDescription(helper.optionDescription(option)),
418
445
  );
419
446
  });
420
447
  if (optionList.length > 0) {
421
- output = output.concat(['Options:', formatList(optionList), '']);
448
+ output = output.concat([
449
+ helper.styleTitle('Options:'),
450
+ ...optionList,
451
+ '',
452
+ ]);
422
453
  }
423
454
 
424
- if (this.showGlobalOptions) {
455
+ if (helper.showGlobalOptions) {
425
456
  const globalOptionList = helper
426
457
  .visibleGlobalOptions(cmd)
427
458
  .map((option) => {
428
- return formatItem(
429
- helper.optionTerm(option),
430
- helper.optionDescription(option),
459
+ return callFormatItem(
460
+ helper.styleOptionTerm(helper.optionTerm(option)),
461
+ helper.styleOptionDescription(helper.optionDescription(option)),
431
462
  );
432
463
  });
433
464
  if (globalOptionList.length > 0) {
434
465
  output = output.concat([
435
- 'Global Options:',
436
- formatList(globalOptionList),
466
+ helper.styleTitle('Global Options:'),
467
+ ...globalOptionList,
437
468
  '',
438
469
  ]);
439
470
  }
@@ -441,18 +472,103 @@ class Help {
441
472
 
442
473
  // Commands
443
474
  const commandList = helper.visibleCommands(cmd).map((cmd) => {
444
- return formatItem(
445
- helper.subcommandTerm(cmd),
446
- helper.subcommandDescription(cmd),
475
+ return callFormatItem(
476
+ helper.styleSubcommandTerm(helper.subcommandTerm(cmd)),
477
+ helper.styleSubcommandDescription(helper.subcommandDescription(cmd)),
447
478
  );
448
479
  });
449
480
  if (commandList.length > 0) {
450
- output = output.concat(['Commands:', formatList(commandList), '']);
481
+ output = output.concat([
482
+ helper.styleTitle('Commands:'),
483
+ ...commandList,
484
+ '',
485
+ ]);
451
486
  }
452
487
 
453
488
  return output.join('\n');
454
489
  }
455
490
 
491
+ /**
492
+ * Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations.
493
+ *
494
+ * @param {string} str
495
+ * @returns {number}
496
+ */
497
+ displayWidth(str) {
498
+ return stripColor(str).length;
499
+ }
500
+
501
+ /**
502
+ * Style the title for displaying in the help. Called with 'Usage:', 'Options:', etc.
503
+ *
504
+ * @param {string} str
505
+ * @returns {string}
506
+ */
507
+ styleTitle(str) {
508
+ return str;
509
+ }
510
+
511
+ styleUsage(str) {
512
+ // Usage has lots of parts the user might like to color separately! Assume default usage string which is formed like:
513
+ // command subcommand [options] [command] <foo> [bar]
514
+ return str
515
+ .split(' ')
516
+ .map((word) => {
517
+ if (word === '[options]') return this.styleOptionText(word);
518
+ if (word === '[command]') return this.styleSubcommandText(word);
519
+ if (word[0] === '[' || word[0] === '<')
520
+ return this.styleArgumentText(word);
521
+ return this.styleCommandText(word); // Restrict to initial words?
522
+ })
523
+ .join(' ');
524
+ }
525
+ styleCommandDescription(str) {
526
+ return this.styleDescriptionText(str);
527
+ }
528
+ styleOptionDescription(str) {
529
+ return this.styleDescriptionText(str);
530
+ }
531
+ styleSubcommandDescription(str) {
532
+ return this.styleDescriptionText(str);
533
+ }
534
+ styleArgumentDescription(str) {
535
+ return this.styleDescriptionText(str);
536
+ }
537
+ styleDescriptionText(str) {
538
+ return str;
539
+ }
540
+ styleOptionTerm(str) {
541
+ return this.styleOptionText(str);
542
+ }
543
+ styleSubcommandTerm(str) {
544
+ // This is very like usage with lots of parts! Assume default string which is formed like:
545
+ // subcommand [options] <foo> [bar]
546
+ return str
547
+ .split(' ')
548
+ .map((word) => {
549
+ if (word === '[options]') return this.styleOptionText(word);
550
+ if (word[0] === '[' || word[0] === '<')
551
+ return this.styleArgumentText(word);
552
+ return this.styleSubcommandText(word); // Restrict to initial words?
553
+ })
554
+ .join(' ');
555
+ }
556
+ styleArgumentTerm(str) {
557
+ return this.styleArgumentText(str);
558
+ }
559
+ styleOptionText(str) {
560
+ return str;
561
+ }
562
+ styleArgumentText(str) {
563
+ return str;
564
+ }
565
+ styleSubcommandText(str) {
566
+ return str;
567
+ }
568
+ styleCommandText(str) {
569
+ return str;
570
+ }
571
+
456
572
  /**
457
573
  * Calculate the pad width from the maximum term length.
458
574
  *
@@ -471,50 +587,123 @@ class Help {
471
587
  }
472
588
 
473
589
  /**
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.
590
+ * Detect manually wrapped and indented strings by checking for line break followed by whitespace.
476
591
  *
477
592
  * @param {string} str
478
- * @param {number} width
479
- * @param {number} indent
480
- * @param {number} [minColumnWidth=40]
481
- * @return {string}
482
- *
593
+ * @returns {boolean}
483
594
  */
595
+ preformatted(str) {
596
+ return /\n[^\S\r\n]/.test(str);
597
+ }
484
598
 
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',
599
+ /**
600
+ * Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
601
+ *
602
+ * So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so:
603
+ * TTT DDD DDDD
604
+ * DD DDD
605
+ *
606
+ * @param {string} term
607
+ * @param {number} termWidth
608
+ * @param {string} description
609
+ * @param {Help} helper
610
+ * @returns {string}
611
+ */
612
+ formatItem(term, termWidth, description, helper) {
613
+ const itemIndent = 2;
614
+ const itemIndentStr = ' '.repeat(itemIndent);
615
+ if (!description) return itemIndentStr + term;
616
+
617
+ // Pad the term out to a consistent width, so descriptions are aligned.
618
+ const paddedTerm = term.padEnd(
619
+ termWidth + term.length - helper.displayWidth(term),
506
620
  );
507
- const lines = columnText.match(regex) || [];
621
+
622
+ // Format the description.
623
+ const spacerWidth = 2; // between term and description
624
+ const helpWidth = this.helpWidth ?? 80; // in case prepareContext() was not called
625
+ const remainingWidth = helpWidth - termWidth - spacerWidth - itemIndent;
626
+ let formattedDescription;
627
+ if (
628
+ remainingWidth < this.minWidthToWrap ||
629
+ helper.preformatted(description)
630
+ ) {
631
+ formattedDescription = description;
632
+ } else {
633
+ const wrappedDescription = helper.boxWrap(description, remainingWidth);
634
+ formattedDescription = wrappedDescription.replace(
635
+ /\n/g,
636
+ '\n' + ' '.repeat(termWidth + spacerWidth),
637
+ );
638
+ }
639
+
640
+ // Construct and overall indent.
508
641
  return (
509
- leadingStr +
510
- lines
511
- .map((line, i) => {
512
- if (line === '\n') return ''; // preserve empty lines
513
- return (i > 0 ? indentString : '') + line.trimEnd();
514
- })
515
- .join('\n')
642
+ itemIndentStr +
643
+ paddedTerm +
644
+ ' '.repeat(spacerWidth) +
645
+ formattedDescription.replace(/\n/g, `\n${itemIndentStr}`)
516
646
  );
517
647
  }
648
+
649
+ /**
650
+ * Wrap a string at whitespace, preserving existing line breaks.
651
+ * Wrapping is skipped if the width is less than `minWidthToWrap`.
652
+ *
653
+ * @param {string} str
654
+ * @param {number} width
655
+ * @returns {string}
656
+ */
657
+ boxWrap(str, width) {
658
+ if (width < this.minWidthToWrap) return str;
659
+
660
+ const rawLines = str.split(/\r\n|\n/);
661
+ // split up text by whitespace
662
+ const chunkPattern = /[\s]*[^\s]+/g;
663
+ const wrappedLines = [];
664
+ rawLines.forEach((line) => {
665
+ const chunks = line.match(chunkPattern);
666
+ if (chunks === null) {
667
+ wrappedLines.push('');
668
+ return;
669
+ }
670
+
671
+ let sumChunks = [chunks.shift()];
672
+ let sumWidth = this.displayWidth(sumChunks[0]);
673
+ chunks.forEach((chunk) => {
674
+ const visibleWidth = this.displayWidth(chunk);
675
+ // Accumulate chunks while they fit into width.
676
+ if (sumWidth + visibleWidth <= width) {
677
+ sumChunks.push(chunk);
678
+ sumWidth += visibleWidth;
679
+ return;
680
+ }
681
+ wrappedLines.push(sumChunks.join(''));
682
+
683
+ const nextChunk = chunk.trimStart(); // trim space at line break
684
+ sumChunks = [nextChunk];
685
+ sumWidth = this.displayWidth(nextChunk);
686
+ });
687
+ wrappedLines.push(sumChunks.join(''));
688
+ });
689
+
690
+ return wrappedLines.join('\n');
691
+ }
692
+ }
693
+
694
+ /**
695
+ * Strip style ANSI escape sequences from the string. In particular, SGR (Select Graphic Rendition) codes.
696
+ *
697
+ * @param {string} str
698
+ * @returns {string}
699
+ * @package
700
+ */
701
+
702
+ function stripColor(str) {
703
+ // eslint-disable-next-line no-control-regex
704
+ const sgrPattern = /\x1b\[\d*(;\d*)*m/g;
705
+ return str.replace(sgrPattern, '');
518
706
  }
519
707
 
520
708
  exports.Help = Help;
709
+ exports.stripColor = stripColor;
package/lib/option.js CHANGED
@@ -207,13 +207,16 @@ class Option {
207
207
 
208
208
  /**
209
209
  * Return option name, in a camelcase format that can be used
210
- * as a object attribute key.
210
+ * as an object attribute key.
211
211
  *
212
212
  * @return {string}
213
213
  */
214
214
 
215
215
  attributeName() {
216
- return camelcase(this.name().replace(/^no-/, ''));
216
+ if (this.negate) {
217
+ return camelcase(this.name().replace(/^no-/, ''));
218
+ }
219
+ return camelcase(this.name());
217
220
  }
218
221
 
219
222
  /**
@@ -312,17 +315,32 @@ function camelcase(str) {
312
315
  function splitOptionFlags(flags) {
313
316
  let shortFlag;
314
317
  let longFlag;
315
- // Use original very loose parsing to maintain backwards compatibility for now,
316
- // which allowed for example unintended `-sw, --short-word` [sic].
317
- const flagParts = flags.split(/[ |,]+/);
318
- if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1]))
319
- shortFlag = flagParts.shift();
320
- longFlag = flagParts.shift();
321
- // Add support for lone short flag without significantly changing parsing!
322
- if (!shortFlag && /^-[^-]$/.test(longFlag)) {
323
- shortFlag = longFlag;
324
- longFlag = undefined;
325
- }
318
+ // short flag, single dash and single character
319
+ const shortFlagExp = /^-[^-]$/;
320
+ // long flag, double dash and at least one character
321
+ const longFlagExp = /^--[^-]/;
322
+
323
+ const flagParts = flags.split(/[ |,]+/).concat('guard');
324
+ if (shortFlagExp.test(flagParts[0])) shortFlag = flagParts.shift();
325
+ if (longFlagExp.test(flagParts[0])) longFlag = flagParts.shift();
326
+
327
+ // Check for some unsupported flags that people try.
328
+ if (/^-[^-][^-]/.test(flagParts[0]))
329
+ throw new Error(
330
+ `invalid Option flags, short option is dash and single character: '${flags}'`,
331
+ );
332
+ if (shortFlag && shortFlagExp.test(flagParts[0]))
333
+ throw new Error(
334
+ `invalid Option flags, more than one short flag: '${flags}'`,
335
+ );
336
+ if (longFlag && longFlagExp.test(flagParts[0]))
337
+ throw new Error(
338
+ `invalid Option flags, more than one long flag: '${flags}'`,
339
+ );
340
+ // Generic error if failed to find a flag or an unexpected flag left over.
341
+ if (!(shortFlag || longFlag) || flagParts[0].startsWith('-'))
342
+ throw new Error(`invalid Option flags: '${flags}'`);
343
+
326
344
  return { shortFlag, longFlag };
327
345
  }
328
346
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commander",
3
- "version": "12.1.0",
3
+ "version": "13.0.0",
4
4
  "description": "the complete solution for node.js command-line programs",
5
5
  "keywords": [
6
6
  "commander",
@@ -60,21 +60,19 @@
60
60
  }
61
61
  },
62
62
  "devDependencies": {
63
- "@eslint/js": "^8.56.0",
63
+ "@eslint/js": "^9.4.0",
64
64
  "@types/jest": "^29.2.4",
65
- "@types/node": "^20.2.5",
66
- "eslint": "^8.30.0",
65
+ "@types/node": "^22.7.4",
66
+ "eslint": "^9.17.0",
67
67
  "eslint-config-prettier": "^9.1.0",
68
68
  "eslint-plugin-jest": "^28.3.0",
69
- "eslint-plugin-jsdoc": "^48.1.0",
70
- "globals": "^13.24.0",
69
+ "globals": "^15.7.0",
71
70
  "jest": "^29.3.1",
72
71
  "prettier": "^3.2.5",
73
- "prettier-plugin-jsdoc": "^1.3.0",
74
72
  "ts-jest": "^29.0.3",
75
73
  "tsd": "^0.31.0",
76
74
  "typescript": "^5.0.4",
77
- "typescript-eslint": "^7.0.1"
75
+ "typescript-eslint": "^8.12.2"
78
76
  },
79
77
  "types": "typings/index.d.ts",
80
78
  "engines": {
@@ -1,8 +1,6 @@
1
1
  // Type definitions for commander
2
2
  // Original definitions by: Alan Agius <https://github.com/alan-agius4>, Marcelo Dezem <https://github.com/mdezem>, vvakame <https://github.com/vvakame>, Jules Randolph <https://github.com/sveinburne>
3
3
 
4
- // Using method rather than property for method-signature-style, to document method overloads separately. Allow either.
5
- /* eslint-disable @typescript-eslint/method-signature-style */
6
4
  /* eslint-disable @typescript-eslint/no-explicit-any */
7
5
 
8
6
  // This is a trick to encourage editor to suggest the known literals while still
@@ -190,7 +188,7 @@ export class Option {
190
188
 
191
189
  /**
192
190
  * Return option name, in a camelcase format that can be used
193
- * as a object attribute key.
191
+ * as an object attribute key.
194
192
  */
195
193
  attributeName(): string;
196
194
 
@@ -205,12 +203,25 @@ export class Option {
205
203
  export class Help {
206
204
  /** output helpWidth, long lines are wrapped to fit */
207
205
  helpWidth?: number;
206
+ minWidthToWrap: number;
208
207
  sortSubcommands: boolean;
209
208
  sortOptions: boolean;
210
209
  showGlobalOptions: boolean;
211
210
 
212
211
  constructor();
213
212
 
213
+ /*
214
+ * prepareContext is called by Commander after applying overrides from `Command.configureHelp()`
215
+ * and just before calling `formatHelp()`.
216
+ *
217
+ * Commander just uses the helpWidth and the others are provided for subclasses.
218
+ */
219
+ prepareContext(contextOptions: {
220
+ error?: boolean;
221
+ helpWidth?: number;
222
+ outputHasColors?: boolean;
223
+ }): void;
224
+
214
225
  /** Get the command term to show in the list of subcommands. */
215
226
  subcommandTerm(cmd: Command): string;
216
227
  /** Get the command summary to show in the list of subcommands. */
@@ -246,18 +257,60 @@ export class Help {
246
257
  longestGlobalOptionTermLength(cmd: Command, helper: Help): number;
247
258
  /** Get the longest argument term length. */
248
259
  longestArgumentTermLength(cmd: Command, helper: Help): number;
260
+
261
+ /** Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations. */
262
+ displayWidth(str: string): number;
263
+
264
+ /** Style the titles. Called with 'Usage:', 'Options:', etc. */
265
+ styleTitle(title: string): string;
266
+
267
+ /** Usage: <str> */
268
+ styleUsage(str: string): string;
269
+ /** Style for command name in usage string. */
270
+ styleCommandText(str: string): string;
271
+
272
+ styleCommandDescription(str: string): string;
273
+ styleOptionDescription(str: string): string;
274
+ styleSubcommandDescription(str: string): string;
275
+ styleArgumentDescription(str: string): string;
276
+ /** Base style used by descriptions. */
277
+ styleDescriptionText(str: string): string;
278
+
279
+ styleOptionTerm(str: string): string;
280
+ styleSubcommandTerm(str: string): string;
281
+ styleArgumentTerm(str: string): string;
282
+
283
+ /** Base style used in terms and usage for options. */
284
+ styleOptionText(str: string): string;
285
+ /** Base style used in terms and usage for subcommands. */
286
+ styleSubcommandText(str: string): string;
287
+ /** Base style used in terms and usage for arguments. */
288
+ styleArgumentText(str: string): string;
289
+
249
290
  /** Calculate the pad width from the maximum term length. */
250
291
  padWidth(cmd: Command, helper: Help): number;
251
292
 
252
293
  /**
253
- * Wrap the given string to width characters per line, with lines after the first indented.
254
- * Do not wrap if insufficient room for wrapping (minColumnWidth), or string is manually formatted.
294
+ * Wrap a string at whitespace, preserving existing line breaks.
295
+ * Wrapping is skipped if the width is less than `minWidthToWrap`.
296
+ */
297
+ boxWrap(str: string, width: number): string;
298
+
299
+ /** Detect manually wrapped and indented strings by checking for line break followed by whitespace. */
300
+ preformatted(str: string): boolean;
301
+
302
+ /**
303
+ * Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
304
+ *
305
+ * So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so:
306
+ * TTT DDD DDDD
307
+ * DD DDD
255
308
  */
256
- wrap(
257
- str: string,
258
- width: number,
259
- indent: number,
260
- minColumnWidth?: number,
309
+ formatItem(
310
+ term: string,
311
+ termWidth: number,
312
+ description: string,
313
+ helper: Help,
261
314
  ): string;
262
315
 
263
316
  /** Generate the built-in help text. */
@@ -280,9 +333,14 @@ export interface AddHelpTextContext {
280
333
  export interface OutputConfiguration {
281
334
  writeOut?(str: string): void;
282
335
  writeErr?(str: string): void;
336
+ outputError?(str: string, write: (str: string) => void): void;
337
+
283
338
  getOutHelpWidth?(): number;
284
339
  getErrHelpWidth?(): number;
285
- outputError?(str: string, write: (str: string) => void): void;
340
+
341
+ getOutHasColors?(): boolean;
342
+ getErrHasColors?(): boolean;
343
+ stripColor?(str: string): string;
286
344
  }
287
345
 
288
346
  export type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll';
@@ -544,7 +602,7 @@ export class Command {
544
602
  *
545
603
  * @returns `this` command for chaining
546
604
  */
547
- action(fn: (...args: any[]) => void | Promise<void>): this;
605
+ action(fn: (this: this, ...args: any[]) => void | Promise<void>): this;
548
606
 
549
607
  /**
550
608
  * Define option with `flags`, `description`, and optional argument parsing function or `defaultValue` or both.
@@ -763,10 +821,28 @@ export class Command {
763
821
  parseOptions?: ParseOptions,
764
822
  ): Promise<this>;
765
823
 
824
+ /**
825
+ * Called the first time parse is called to save state and allow a restore before subsequent calls to parse.
826
+ * Not usually called directly, but available for subclasses to save their custom state.
827
+ *
828
+ * This is called in a lazy way. Only commands used in parsing chain will have state saved.
829
+ */
830
+ saveStateBeforeParse(): void;
831
+
832
+ /**
833
+ * Restore state before parse for calls after the first.
834
+ * Not usually called directly, but available for subclasses to save their custom state.
835
+ *
836
+ * This is called in a lazy way. Only commands used in parsing chain will have state restored.
837
+ */
838
+ restoreStateBeforeParse(): void;
839
+
766
840
  /**
767
841
  * Parse options from `argv` removing known options,
768
842
  * and return argv split into operands and unknown arguments.
769
843
  *
844
+ * Side effects: modifies command by storing options. Does not reset state if called again.
845
+ *
770
846
  * argv => operands, unknown
771
847
  * --known kkk op => [op], []
772
848
  * op --known kkk => [op], []