commander 12.1.0 → 13.0.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 });
@@ -923,23 +923,7 @@ program.helpCommand('assist [command]', 'show assistance');
923
923
  The built-in help is formatted using the Help class.
924
924
  You can configure the Help behaviour by modifying data properties and methods using `.configureHelp()`, or by subclassing using `.createHelp()` if you prefer.
925
925
 
926
- The data properties are:
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
- ```
926
+ For more detail see (./docs/help-in-depth.md)
943
927
 
944
928
  ## Custom event listeners
945
929
 
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
@@ -56,15 +56,20 @@ class Command extends EventEmitter {
56
56
  this._showHelpAfterError = false;
57
57
  this._showSuggestionAfterError = true;
58
58
 
59
- // see .configureOutput() for docs
59
+ // see configureOutput() for docs
60
60
  this._outputConfiguration = {
61
61
  writeOut: (str) => process.stdout.write(str),
62
62
  writeErr: (str) => process.stderr.write(str),
63
+ outputError: (str, write) => write(str),
63
64
  getOutHelpWidth: () =>
64
65
  process.stdout.isTTY ? process.stdout.columns : undefined,
65
66
  getErrHelpWidth: () =>
66
67
  process.stderr.isTTY ? process.stderr.columns : undefined,
67
- outputError: (str, write) => write(str),
68
+ getOutHasColors: () =>
69
+ useColor() ?? (process.stdout.isTTY && process.stdout.hasColors?.()),
70
+ getErrHasColors: () =>
71
+ useColor() ?? (process.stderr.isTTY && process.stderr.hasColors?.()),
72
+ stripColor: (str) => stripColor(str),
68
73
  };
69
74
 
70
75
  this._hidden = false;
@@ -213,14 +218,18 @@ class Command extends EventEmitter {
213
218
  *
214
219
  * The configuration properties are all functions:
215
220
  *
216
- * // functions to change where being written, stdout and stderr
221
+ * // change how output being written, defaults to stdout and stderr
217
222
  * writeOut(str)
218
223
  * writeErr(str)
219
- * // matching functions to specify width for wrapping help
224
+ * // change how output being written for errors, defaults to writeErr
225
+ * outputError(str, write) // used for displaying errors and not used for displaying help
226
+ * // specify width for wrapping help
220
227
  * getOutHelpWidth()
221
228
  * getErrHelpWidth()
222
- * // functions based on what is being written out
223
- * outputError(str, write) // used for displaying errors, and not used for displaying help
229
+ * // color support, currently only used with Help
230
+ * getOutHasColors()
231
+ * getErrHasColors()
232
+ * stripColor() // used to remove ANSI escape codes if output does not have colors
224
233
  *
225
234
  * @param {object} [configuration] - configuration options
226
235
  * @return {(Command | object)} `this` command for chaining, or stored configuration
@@ -1134,7 +1143,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1134
1143
  let resolvedScriptPath; // resolve possible symlink for installed npm binary
1135
1144
  try {
1136
1145
  resolvedScriptPath = fs.realpathSync(this._scriptPath);
1137
- } catch (err) {
1146
+ } catch {
1138
1147
  resolvedScriptPath = this._scriptPath;
1139
1148
  }
1140
1149
  executableDir = path.resolve(
@@ -2254,31 +2263,49 @@ Expecting one of '${allowedValues.join("', '")}'`);
2254
2263
 
2255
2264
  helpInformation(contextOptions) {
2256
2265
  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);
2266
+ const context = this._getOutputContext(contextOptions);
2267
+ helper.prepareContext({
2268
+ error: context.error,
2269
+ helpWidth: context.helpWidth,
2270
+ outputHasColors: context.hasColors,
2271
+ });
2272
+ const text = helper.formatHelp(this, helper);
2273
+ if (context.hasColors) return text;
2274
+ return this._outputConfiguration.stripColor(text);
2264
2275
  }
2265
2276
 
2266
2277
  /**
2278
+ * @typedef HelpContext
2279
+ * @type {object}
2280
+ * @property {boolean} error
2281
+ * @property {number} helpWidth
2282
+ * @property {boolean} hasColors
2283
+ * @property {function} write - includes stripColor if needed
2284
+ *
2285
+ * @returns {HelpContext}
2267
2286
  * @private
2268
2287
  */
2269
2288
 
2270
- _getHelpContext(contextOptions) {
2289
+ _getOutputContext(contextOptions) {
2271
2290
  contextOptions = contextOptions || {};
2272
- const context = { error: !!contextOptions.error };
2273
- let write;
2274
- if (context.error) {
2275
- write = (arg) => this._outputConfiguration.writeErr(arg);
2291
+ const error = !!contextOptions.error;
2292
+ let baseWrite;
2293
+ let hasColors;
2294
+ let helpWidth;
2295
+ if (error) {
2296
+ baseWrite = (str) => this._outputConfiguration.writeErr(str);
2297
+ hasColors = this._outputConfiguration.getErrHasColors();
2298
+ helpWidth = this._outputConfiguration.getErrHelpWidth();
2276
2299
  } else {
2277
- write = (arg) => this._outputConfiguration.writeOut(arg);
2300
+ baseWrite = (str) => this._outputConfiguration.writeOut(str);
2301
+ hasColors = this._outputConfiguration.getOutHasColors();
2302
+ helpWidth = this._outputConfiguration.getOutHelpWidth();
2278
2303
  }
2279
- context.write = contextOptions.write || write;
2280
- context.command = this;
2281
- return context;
2304
+ const write = (str) => {
2305
+ if (!hasColors) str = this._outputConfiguration.stripColor(str);
2306
+ return baseWrite(str);
2307
+ };
2308
+ return { error, write, hasColors, helpWidth };
2282
2309
  }
2283
2310
 
2284
2311
  /**
@@ -2295,14 +2322,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
2295
2322
  deprecatedCallback = contextOptions;
2296
2323
  contextOptions = undefined;
2297
2324
  }
2298
- const context = this._getHelpContext(contextOptions);
2325
+
2326
+ const outputContext = this._getOutputContext(contextOptions);
2327
+ /** @type {HelpTextEventContext} */
2328
+ const eventContext = {
2329
+ error: outputContext.error,
2330
+ write: outputContext.write,
2331
+ command: this,
2332
+ };
2299
2333
 
2300
2334
  this._getCommandAndAncestors()
2301
2335
  .reverse()
2302
- .forEach((command) => command.emit('beforeAllHelp', context));
2303
- this.emit('beforeHelp', context);
2336
+ .forEach((command) => command.emit('beforeAllHelp', eventContext));
2337
+ this.emit('beforeHelp', eventContext);
2304
2338
 
2305
- let helpInformation = this.helpInformation(context);
2339
+ let helpInformation = this.helpInformation({ error: outputContext.error });
2306
2340
  if (deprecatedCallback) {
2307
2341
  helpInformation = deprecatedCallback(helpInformation);
2308
2342
  if (
@@ -2312,14 +2346,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
2312
2346
  throw new Error('outputHelp callback must return a string or a Buffer');
2313
2347
  }
2314
2348
  }
2315
- context.write(helpInformation);
2349
+ outputContext.write(helpInformation);
2316
2350
 
2317
2351
  if (this._getHelpOption()?.long) {
2318
2352
  this.emit(this._getHelpOption().long); // deprecated
2319
2353
  }
2320
- this.emit('afterHelp', context);
2354
+ this.emit('afterHelp', eventContext);
2321
2355
  this._getCommandAndAncestors().forEach((command) =>
2322
- command.emit('afterAllHelp', context),
2356
+ command.emit('afterAllHelp', eventContext),
2323
2357
  );
2324
2358
  }
2325
2359
 
@@ -2339,6 +2373,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
2339
2373
  helpOption(flags, description) {
2340
2374
  // Support disabling built-in help option.
2341
2375
  if (typeof flags === 'boolean') {
2376
+ // true is not an expected value. Do something sensible but no unit-test.
2377
+ // istanbul ignore if
2342
2378
  if (flags) {
2343
2379
  this._helpOption = this._helpOption ?? undefined; // preserve existing option
2344
2380
  } else {
@@ -2392,7 +2428,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2392
2428
 
2393
2429
  help(contextOptions) {
2394
2430
  this.outputHelp(contextOptions);
2395
- let exitCode = process.exitCode || 0;
2431
+ let exitCode = Number(process.exitCode ?? 0); // process.exitCode does allow a string or an integer, but we prefer just a number
2396
2432
  if (
2397
2433
  exitCode === 0 &&
2398
2434
  contextOptions &&
@@ -2405,6 +2441,15 @@ Expecting one of '${allowedValues.join("', '")}'`);
2405
2441
  this._exit(exitCode, 'commander.help', '(outputHelp)');
2406
2442
  }
2407
2443
 
2444
+ /**
2445
+ * // Do a little typing to coordinate emit and listener for the help text events.
2446
+ * @typedef HelpTextEventContext
2447
+ * @type {object}
2448
+ * @property {boolean} error
2449
+ * @property {Command} command
2450
+ * @property {function} write
2451
+ */
2452
+
2408
2453
  /**
2409
2454
  * Add additional text to be displayed with the built-in help.
2410
2455
  *
@@ -2415,14 +2460,16 @@ Expecting one of '${allowedValues.join("', '")}'`);
2415
2460
  * @param {(string | Function)} text - string to add, or a function returning a string
2416
2461
  * @return {Command} `this` command for chaining
2417
2462
  */
2463
+
2418
2464
  addHelpText(position, text) {
2419
2465
  const allowedValues = ['beforeAll', 'before', 'after', 'afterAll'];
2420
2466
  if (!allowedValues.includes(position)) {
2421
2467
  throw new Error(`Unexpected value for position to addHelpText.
2422
2468
  Expecting one of '${allowedValues.join("', '")}'`);
2423
2469
  }
2470
+
2424
2471
  const helpEvent = `${position}Help`;
2425
- this.on(helpEvent, (context) => {
2472
+ this.on(helpEvent, (/** @type {HelpTextEventContext} */ context) => {
2426
2473
  let helpStr;
2427
2474
  if (typeof text === 'function') {
2428
2475
  helpStr = text({ error: context.error, command: context.command });
@@ -2506,4 +2553,33 @@ function incrementNodeInspectorPort(args) {
2506
2553
  });
2507
2554
  }
2508
2555
 
2556
+ /**
2557
+ * @returns {boolean | undefined}
2558
+ * @package
2559
+ */
2560
+ function useColor() {
2561
+ // Test for common conventions.
2562
+ // NB: the observed behaviour is in combination with how author adds color! For example:
2563
+ // - we do not test NODE_DISABLE_COLORS, but util:styletext does
2564
+ // - we do test NO_COLOR, but Chalk does not
2565
+ //
2566
+ // References:
2567
+ // https://no-color.org
2568
+ // https://bixense.com/clicolors/
2569
+ // https://github.com/nodejs/node/blob/0a00217a5f67ef4a22384cfc80eb6dd9a917fdc1/lib/internal/tty.js#L109
2570
+ // https://github.com/chalk/supports-color/blob/c214314a14bcb174b12b3014b2b0a8de375029ae/index.js#L33
2571
+ // (https://force-color.org recent web page from 2023, does not match major javascript implementations)
2572
+
2573
+ if (
2574
+ process.env.NO_COLOR ||
2575
+ process.env.FORCE_COLOR === '0' ||
2576
+ process.env.FORCE_COLOR === 'false'
2577
+ )
2578
+ return false;
2579
+ if (process.env.FORCE_COLOR || process.env.CLICOLOR_FORCE !== undefined)
2580
+ return true;
2581
+ return undefined;
2582
+ }
2583
+
2509
2584
  exports.Command = Command;
2585
+ 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-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": "^8.57.1",
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": {
@@ -190,7 +190,7 @@ export class Option {
190
190
 
191
191
  /**
192
192
  * Return option name, in a camelcase format that can be used
193
- * as a object attribute key.
193
+ * as an object attribute key.
194
194
  */
195
195
  attributeName(): string;
196
196
 
@@ -205,12 +205,25 @@ export class Option {
205
205
  export class Help {
206
206
  /** output helpWidth, long lines are wrapped to fit */
207
207
  helpWidth?: number;
208
+ minWidthToWrap: number;
208
209
  sortSubcommands: boolean;
209
210
  sortOptions: boolean;
210
211
  showGlobalOptions: boolean;
211
212
 
212
213
  constructor();
213
214
 
215
+ /*
216
+ * prepareContext is called by Commander after applying overrides from `Command.configureHelp()`
217
+ * and just before calling `formatHelp()`.
218
+ *
219
+ * Commander just uses the helpWidth and the others are provided for subclasses.
220
+ */
221
+ prepareContext(contextOptions: {
222
+ error?: boolean;
223
+ helpWidth?: number;
224
+ outputHasColors?: boolean;
225
+ }): void;
226
+
214
227
  /** Get the command term to show in the list of subcommands. */
215
228
  subcommandTerm(cmd: Command): string;
216
229
  /** Get the command summary to show in the list of subcommands. */
@@ -246,18 +259,60 @@ export class Help {
246
259
  longestGlobalOptionTermLength(cmd: Command, helper: Help): number;
247
260
  /** Get the longest argument term length. */
248
261
  longestArgumentTermLength(cmd: Command, helper: Help): number;
262
+
263
+ /** Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations. */
264
+ displayWidth(str: string): number;
265
+
266
+ /** Style the titles. Called with 'Usage:', 'Options:', etc. */
267
+ styleTitle(title: string): string;
268
+
269
+ /** Usage: <str> */
270
+ styleUsage(str: string): string;
271
+ /** Style for command name in usage string. */
272
+ styleCommandText(str: string): string;
273
+
274
+ styleCommandDescription(str: string): string;
275
+ styleOptionDescription(str: string): string;
276
+ styleSubcommandDescription(str: string): string;
277
+ styleArgumentDescription(str: string): string;
278
+ /** Base style used by descriptions. */
279
+ styleDescriptionText(str: string): string;
280
+
281
+ styleOptionTerm(str: string): string;
282
+ styleSubcommandTerm(str: string): string;
283
+ styleArgumentTerm(str: string): string;
284
+
285
+ /** Base style used in terms and usage for options. */
286
+ styleOptionText(str: string): string;
287
+ /** Base style used in terms and usage for subcommands. */
288
+ styleSubcommandText(str: string): string;
289
+ /** Base style used in terms and usage for arguments. */
290
+ styleArgumentText(str: string): string;
291
+
249
292
  /** Calculate the pad width from the maximum term length. */
250
293
  padWidth(cmd: Command, helper: Help): number;
251
294
 
252
295
  /**
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.
296
+ * Wrap a string at whitespace, preserving existing line breaks.
297
+ * Wrapping is skipped if the width is less than `minWidthToWrap`.
255
298
  */
256
- wrap(
257
- str: string,
258
- width: number,
259
- indent: number,
260
- minColumnWidth?: number,
299
+ boxWrap(str: string, width: number): string;
300
+
301
+ /** Detect manually wrapped and indented strings by checking for line break followed by whitespace. */
302
+ preformatted(str: string): boolean;
303
+
304
+ /**
305
+ * Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
306
+ *
307
+ * So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so:
308
+ * TTT DDD DDDD
309
+ * DD DDD
310
+ */
311
+ formatItem(
312
+ term: string,
313
+ termWidth: number,
314
+ description: string,
315
+ helper: Help,
261
316
  ): string;
262
317
 
263
318
  /** Generate the built-in help text. */
@@ -280,9 +335,14 @@ export interface AddHelpTextContext {
280
335
  export interface OutputConfiguration {
281
336
  writeOut?(str: string): void;
282
337
  writeErr?(str: string): void;
338
+ outputError?(str: string, write: (str: string) => void): void;
339
+
283
340
  getOutHelpWidth?(): number;
284
341
  getErrHelpWidth?(): number;
285
- outputError?(str: string, write: (str: string) => void): void;
342
+
343
+ getOutHasColors?(): boolean;
344
+ getErrHasColors?(): boolean;
345
+ stripColor?(str: string): string;
286
346
  }
287
347
 
288
348
  export type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll';
@@ -544,7 +604,7 @@ export class Command {
544
604
  *
545
605
  * @returns `this` command for chaining
546
606
  */
547
- action(fn: (...args: any[]) => void | Promise<void>): this;
607
+ action(fn: (this: this, ...args: any[]) => void | Promise<void>): this;
548
608
 
549
609
  /**
550
610
  * Define option with `flags`, `description`, and optional argument parsing function or `defaultValue` or both.