From 8f06ce56d940869d8f9b46e7d9557c310f4081bc Mon Sep 17 00:00:00 2001 From: Adrian Estrada Date: Mon, 13 Jul 2026 20:56:56 -0500 Subject: [PATCH 1/2] cli: style node --help output with util.styleText Apply util.styleText to the `node --help` output for improved visual hierarchy and scannability. Styling is applied only when the output stream supports color (detected via util.styleText's built-in shouldColorize), so piped/redirected output and NO_COLOR remain plain text with no behavior change. - Bold: Usage lines, Options:, Environment variables: headers - Bold green: CLI option names (e.g. --inspect, -e, --eval) - Bold magenta: environment variable names (e.g. NODE_PATH) - Dim: "(currently set)" annotation - Blue underline: documentation URL Column-width math uses unstyled string lengths to preserve alignment regardless of styling. The "(currently set)" annotation is styled via post-processing after layout to avoid breaking the fold() width calculation. Signed-off-by: Adrian Estrada --- lib/internal/main/print_help.js | 33 +++++++--- .../test-cli-node-print-help-style.js | 62 +++++++++++++++++++ 2 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 test/parallel/test-cli-node-print-help-style.js diff --git a/lib/internal/main/print_help.js b/lib/internal/main/print_help.js index 62269956be9faf..d788c823fb2c13 100644 --- a/lib/internal/main/print_help.js +++ b/lib/internal/main/print_help.js @@ -25,11 +25,16 @@ const { } = require('internal/process/pre_execution'); const { getCLIOptionsInfo, getOptionValue } = require('internal/options'); +const { styleText } = require('util'); const typeLookup = []; for (const key of ObjectKeys(types)) typeLookup[types[key]] = key; +function style(format, text) { + return styleText(format, text, { stream: process.stdout }); +} + // Environment variables are parsed ad-hoc throughout the code base, // so we gather the documentation here. const { hasIntl, hasSmallICU, hasNodeOptions } = internalBinding('config'); @@ -117,7 +122,7 @@ function getArgDescription(type) { } function format( - { options, aliases = new SafeMap(), firstColumn, secondColumn }, + { options, aliases = new SafeMap(), firstColumn, secondColumn, nameStyle = [] }, ) { let text = ''; let maxFirstColumnUsed = 0; @@ -176,7 +181,7 @@ function format( displayHelpText += ' (currently set)'; } - text += displayName; + text += style(nameStyle, displayName); maxFirstColumnUsed = MathMax(maxFirstColumnUsed, displayName.length); if (displayName.length >= firstColumn) text += '\n' + StringPrototypeRepeat(' ', firstColumn); @@ -194,6 +199,7 @@ function format( aliases, firstColumn: maxFirstColumnUsed + 2, secondColumn, + nameStyle, }); } @@ -214,20 +220,29 @@ function print(stream) { 'interactive mode if a tty)' }); options.set('--', { helpText: 'indicate the end of node options' }); let helpText = ( - 'Usage: node [options] [ script.js ] [arguments]\n' + - ' node inspect [options] [ script.js | host:port ] [arguments]\n\n' + - 'Options:\n'); + style('bold', + 'Usage: node [options] [ script.js ] [arguments]\n' + + ' node inspect [options] [ script.js | host:port ] [arguments]') + + '\n\n' + style('bold', 'Options:') + '\n'); helpText += (indent(format({ - options, aliases, firstColumn, secondColumn, + options, aliases, firstColumn, secondColumn, nameStyle: ['bold', 'green'], }), 2)); - helpText += ('\nEnvironment variables:\n'); + helpText += ('\n' + style('bold', 'Environment variables:') + '\n'); helpText += (format({ - options: envVars, firstColumn, secondColumn, + options: envVars, firstColumn, secondColumn, nameStyle: ['bold', 'magenta'], })); - helpText += ('\nDocumentation can be found at https://nodejs.org/'); + helpText += ('\nDocumentation can be found at ' + + style(['blue', 'underline'], 'https://nodejs.org/')); + + helpText = RegExpPrototypeSymbolReplace( + / \(currently set\)/g, + helpText, + style('dim', ' (currently set)'), + ); + console.log(helpText); } diff --git a/test/parallel/test-cli-node-print-help-style.js b/test/parallel/test-cli-node-print-help-style.js new file mode 100644 index 00000000000000..df307d1750f51c --- /dev/null +++ b/test/parallel/test-cli-node-print-help-style.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { execFile } = require('child_process'); + +// eslint-disable-next-line no-control-regex +const ANSI_SGR_REGEX = new RegExp('\x1b\\[[0-9;]*m', 'g'); + +function stripAnsi(text) { + return text.replace(ANSI_SGR_REGEX, ''); +} + +// Test: FORCE_COLOR=1 produces styled output +{ + const env = { ...process.env, FORCE_COLOR: '1' }; + execFile(process.execPath, ['--help'], { env }, + common.mustSucceed((stdout) => { + assert.ok(stdout.includes('\x1b[1m'), 'bold for headers should be present'); + assert.ok(stdout.includes('\x1b[32m'), 'green for option names should be present'); + assert.ok(stdout.includes('\x1b[35m'), 'magenta for env var names should be present'); + assert.ok(stdout.includes('\x1b[34m'), 'blue for URL should be present'); + assert.ok(stdout.includes('\x1b[4m'), 'underline for URL should be present'); + })); +} + +// Test: NO_COLOR=1 produces plain output +{ + const env = { ...process.env, NO_COLOR: '1' }; + execFile(process.execPath, ['--help'], { env }, + common.mustSucceed((stdout) => { + assert.ok(stripAnsi(stdout) === stdout, + 'no ANSI escape sequences should be present with NO_COLOR=1'); + })); +} + +// Test: piped (non-TTY, no FORCE_COLOR) produces plain output +{ + const env = { ...process.env }; + delete env.FORCE_COLOR; + delete env.NO_COLOR; + delete env.NODE_DISABLE_COLORS; + execFile(process.execPath, ['--help'], { env }, + common.mustSucceed((stdout) => { + assert.ok(stripAnsi(stdout) === stdout, + 'no ANSI escape sequences should be present when piped (non-TTY)'); + })); +} + +// Test: alignment preservation - stripped styled output matches plain output +{ + const envStyled = { ...process.env, FORCE_COLOR: '1' }; + const envPlain = { ...process.env, NO_COLOR: '1' }; + execFile(process.execPath, ['--help'], { env: envStyled }, + common.mustSucceed((styledStdout) => { + execFile(process.execPath, ['--help'], { env: envPlain }, + common.mustSucceed((plainStdout) => { + assert.ok(stripAnsi(styledStdout) === plainStdout, + 'stripped styled output should match plain output (alignment preservation)'); + })); + })); +} From 97df7806256918b82b817c80eb9e6df1764648eb Mon Sep 17 00:00:00 2001 From: Adrian Estrada Date: Tue, 14 Jul 2026 15:03:03 -0500 Subject: [PATCH 2/2] test: fix help style test when FORCE_COLOR is in parent env The NO_COLOR and alignment-preservation tests inherited FORCE_COLOR from process.env, which takes precedence over NO_COLOR and caused the tests to fail on CI environments where FORCE_COLOR is set. Delete FORCE_COLOR from the child env in those test cases. Signed-off-by: Adrian Estrada --- test/parallel/test-cli-node-print-help-style.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/parallel/test-cli-node-print-help-style.js b/test/parallel/test-cli-node-print-help-style.js index df307d1750f51c..47fe7b75620a8d 100644 --- a/test/parallel/test-cli-node-print-help-style.js +++ b/test/parallel/test-cli-node-print-help-style.js @@ -27,6 +27,7 @@ function stripAnsi(text) { // Test: NO_COLOR=1 produces plain output { const env = { ...process.env, NO_COLOR: '1' }; + delete env.FORCE_COLOR; execFile(process.execPath, ['--help'], { env }, common.mustSucceed((stdout) => { assert.ok(stripAnsi(stdout) === stdout, @@ -51,6 +52,7 @@ function stripAnsi(text) { { const envStyled = { ...process.env, FORCE_COLOR: '1' }; const envPlain = { ...process.env, NO_COLOR: '1' }; + delete envPlain.FORCE_COLOR; execFile(process.execPath, ['--help'], { env: envStyled }, common.mustSucceed((styledStdout) => { execFile(process.execPath, ['--help'], { env: envPlain },