@thegetty/quire-cli 1.0.0-rc.39 → 1.0.0-rc.40

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/bin/cli.js CHANGED
@@ -20,7 +20,20 @@ process.removeAllListeners('warning')
20
20
  process.env.QUIRE_LOG_LEVEL = config.get('logLevel') || 'info'
21
21
 
22
22
  /**
23
- * Dynamic import ensures env var is set before logger modules are loaded
23
+ * Set NO_COLOR env var from config before importing CLI modules
24
+ *
25
+ * Chalk 5.x and ora read NO_COLOR at module load time.
26
+ * Setting it here ensures color is disabled before any styled imports.
27
+ * If NO_COLOR is already set in the shell environment, we do not override it.
28
+ *
29
+ * @see https://no-color.org/
30
+ */
31
+ if (process.env.NO_COLOR === undefined && config.get('logUseColor') === false) {
32
+ process.env.NO_COLOR = '1'
33
+ }
34
+
35
+ /**
36
+ * Dynamic import ensures env vars are set before logger modules are loaded
24
37
  */
25
38
  const { default: cli } = await import('#src/main.js')
26
39
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thegetty/quire-cli",
3
3
  "description": "Quire command-line interface",
4
- "version": "1.0.0-rc.39",
4
+ "version": "1.0.0-rc.40",
5
5
  "author": "Getty Digital",
6
6
  "license": "SEE LICENSE IN https://github.com/thegetty/quire/blob/main/LICENSE",
7
7
  "bugs": {
@@ -205,7 +205,7 @@ Examples:
205
205
  const output = formatSettings(this.config.store, {
206
206
  showInternal: options.debug,
207
207
  configPath: this.config.path,
208
- useColor: this.config.get('logUseColor'),
208
+ useColor: this.config.get('logUseColor') && !process.env.NO_COLOR,
209
209
  })
210
210
  this.logger.info(output)
211
211
  }
@@ -10,6 +10,8 @@ import { Argument, Option } from 'commander'
10
10
 
11
11
  // Re-export shared option definitions
12
12
  export {
13
+ colorOption,
14
+ noColorOption,
13
15
  quietOption,
14
16
  verboseOption,
15
17
  debugOption,
@@ -124,6 +124,30 @@ export const debugOption = [
124
124
  { conflicts: ['quiet'] }
125
125
  ]
126
126
 
127
+ /**
128
+ * Color options - enable/disable colored output
129
+ *
130
+ * Commander.js requires separate option definitions for --color and --no-color
131
+ * to support both the positive and negative forms. When --color has no default,
132
+ * the three-state semantics are:
133
+ *
134
+ * - `--color` → options.color = true (force color on)
135
+ * - `--no-color` → options.color = false (force color off)
136
+ * - (no flag) → options.color = undefined (falls back to env/config)
137
+ *
138
+ * Respects the NO_COLOR environment variable (https://no-color.org/).
139
+ * Respects config default: `quire settings set logUseColor false`
140
+ *
141
+ * @type {Array[]}
142
+ */
143
+ export const colorOption = [
144
+ '--color', 'force colored output (overrides NO_COLOR env var)',
145
+ ]
146
+
147
+ export const noColorOption = [
148
+ '--no-color', 'disable colored output',
149
+ ]
150
+
127
151
  /**
128
152
  * Standard output mode options (quiet, verbose, debug, progress)
129
153
  *
@@ -1,7 +1,7 @@
1
1
  import test from 'ava'
2
2
  import { Command } from 'commander'
3
3
  import { arrayToOption } from './index.js'
4
- import { quietOption, verboseOption, debugOption } from './options.js'
4
+ import { colorOption, noColorOption, quietOption, verboseOption, debugOption } from './options.js'
5
5
 
6
6
  // ─────────────────────────────────────────────────────────────────────────────
7
7
  // Integration tests for option conflicts
@@ -16,6 +16,8 @@ import { quietOption, verboseOption, debugOption } from './options.js'
16
16
  function createTestProgram() {
17
17
  const program = new Command()
18
18
  program
19
+ .addOption(arrayToOption(colorOption))
20
+ .addOption(arrayToOption(noColorOption))
19
21
  .addOption(arrayToOption(quietOption))
20
22
  .addOption(arrayToOption(verboseOption))
21
23
  .addOption(arrayToOption(debugOption))
@@ -107,3 +109,46 @@ test('options order does not affect conflict detection', (t) => {
107
109
  t.throws(() => program1.parse(['node', 'test', '--quiet', '--verbose']))
108
110
  t.throws(() => program2.parse(['node', 'test', '--verbose', '--quiet']))
109
111
  })
112
+
113
+ // ─────────────────────────────────────────────────────────────────────────────
114
+ // Color option tests
115
+ // ─────────────────────────────────────────────────────────────────────────────
116
+
117
+ test('--no-color sets color to false', (t) => {
118
+ const program = createTestProgram()
119
+ program.parse(['node', 'test', '--no-color'])
120
+ t.is(program.opts().color, false)
121
+ })
122
+
123
+ test('--color sets color to true', (t) => {
124
+ const program = createTestProgram()
125
+ program.parse(['node', 'test', '--color'])
126
+ t.is(program.opts().color, true)
127
+ })
128
+
129
+ test('no color flag leaves color undefined', (t) => {
130
+ const program = createTestProgram()
131
+ program.parse(['node', 'test'])
132
+ t.is(program.opts().color, undefined)
133
+ })
134
+
135
+ test('--no-color can be combined with --verbose', (t) => {
136
+ const program = createTestProgram()
137
+ t.notThrows(() => {
138
+ program.parse(['node', 'test', '--no-color', '--verbose'])
139
+ })
140
+ })
141
+
142
+ test('--no-color can be combined with --quiet', (t) => {
143
+ const program = createTestProgram()
144
+ t.notThrows(() => {
145
+ program.parse(['node', 'test', '--no-color', '--quiet'])
146
+ })
147
+ })
148
+
149
+ test('--no-color can be combined with --debug', (t) => {
150
+ const program = createTestProgram()
151
+ t.notThrows(() => {
152
+ program.parse(['node', 'test', '--no-color', '--debug'])
153
+ })
154
+ })
@@ -162,7 +162,7 @@ export default function createLogger(name = 'quire', level) {
162
162
  const prefix = config.get('logPrefix')
163
163
  const prefixStyle = config.get('logPrefixStyle')
164
164
  const showLevel = config.get('logShowLevel')
165
- const useColor = config.get('logUseColor')
165
+ const useColor = config.get('logUseColor') && !process.env.NO_COLOR
166
166
  const colorMessages = config.get('logColorMessages')
167
167
 
168
168
  const parts = []
@@ -475,3 +475,104 @@ test.serial('invalid env var value falls back to info', async (t) => {
475
475
  }
476
476
  }
477
477
  })
478
+
479
+ // ─────────────────────────────────────────────────────────────────────────────
480
+ // NO_COLOR environment variable tests
481
+ // ─────────────────────────────────────────────────────────────────────────────
482
+
483
+ test.serial('logger disables color when NO_COLOR env var is set', async (t) => {
484
+ const { sandbox } = t.context
485
+ const consoleErrorStub = sandbox.stub(console, 'error')
486
+ const originalNoColor = process.env.NO_COLOR
487
+
488
+ try {
489
+ process.env.NO_COLOR = '1'
490
+
491
+ const mockConfig = {
492
+ get: sandbox.stub().callsFake((key) => ({
493
+ logPrefix: 'quire',
494
+ logPrefixStyle: 'bracket',
495
+ logShowLevel: true,
496
+ logUseColor: true, // Config says use color, but NO_COLOR overrides
497
+ logColorMessages: true,
498
+ })[key])
499
+ }
500
+
501
+ const { default: createLoggerMocked } = await esmock('./index.js', {
502
+ '#lib/conf/config.js': { default: mockConfig }
503
+ })
504
+
505
+ const log = createLoggerMocked('test:nocolor', 'error')
506
+ log.error('Error message')
507
+
508
+ t.true(consoleErrorStub.calledOnce)
509
+ const output = consoleErrorStub.firstCall.args.join(' ')
510
+ // Should not contain ANSI escape sequences when NO_COLOR is set
511
+ t.false(/\u001b\[/.test(output), 'output should not contain ANSI escape codes')
512
+ // Should still contain the actual text
513
+ t.true(output.includes('[quire]'))
514
+ t.true(output.includes('ERROR'))
515
+ t.true(output.includes('Error message'))
516
+ } finally {
517
+ if (originalNoColor === undefined) {
518
+ delete process.env.NO_COLOR
519
+ } else {
520
+ process.env.NO_COLOR = originalNoColor
521
+ }
522
+ }
523
+ })
524
+
525
+ test.serial('logger uses color when NO_COLOR env var is not set and config enables color', async (t) => {
526
+ const { sandbox } = t.context
527
+ const consoleErrorStub = sandbox.stub(console, 'error')
528
+ const originalNoColor = process.env.NO_COLOR
529
+
530
+ try {
531
+ delete process.env.NO_COLOR
532
+
533
+ const mockConfig = {
534
+ get: sandbox.stub().callsFake((key) => ({
535
+ logPrefix: 'quire',
536
+ logPrefixStyle: 'bracket',
537
+ logShowLevel: true,
538
+ logUseColor: true,
539
+ logColorMessages: true,
540
+ })[key])
541
+ }
542
+
543
+ // Create a mock chalk that always applies ANSI styling regardless of TTY/env
544
+ const ansiRed = (s) => `\u001b[91m${s}\u001b[39m`
545
+ const ansiRedInverse = (s) => `\u001b[91m\u001b[7m${s}\u001b[27m\u001b[39m`
546
+ const ansiBold = (s) => `\u001b[1m${s}\u001b[22m`
547
+ const mockChalk = {
548
+ bold: ansiBold,
549
+ gray: (s) => s,
550
+ yellow: (s) => s,
551
+ magenta: (s) => s,
552
+ red: ansiRed,
553
+ redBright: { inverse: ansiRedInverse },
554
+ 'yellow.inverse': (s) => s,
555
+ }
556
+ // Support chained properties
557
+ mockChalk.yellow.inverse = (s) => s
558
+
559
+ const { default: createLoggerMocked } = await esmock('./index.js', {
560
+ '#lib/conf/config.js': { default: mockConfig },
561
+ 'chalk': { default: mockChalk }
562
+ })
563
+
564
+ const log = createLoggerMocked('test:withcolor', 'error')
565
+ log.error('Error message')
566
+
567
+ t.true(consoleErrorStub.calledOnce)
568
+ const output = consoleErrorStub.firstCall.args.join(' ')
569
+ // Should contain ANSI escape sequences when color is enabled and NO_COLOR not set
570
+ t.true(/\u001b\[/.test(output), 'output should contain ANSI escape codes')
571
+ } finally {
572
+ if (originalNoColor === undefined) {
573
+ delete process.env.NO_COLOR
574
+ } else {
575
+ process.env.NO_COLOR = originalNoColor
576
+ }
577
+ }
578
+ })
package/src/main.js CHANGED
@@ -2,6 +2,8 @@ import { Command, Argument, Option } from 'commander'
2
2
  import {
3
3
  arrayToArgument,
4
4
  arrayToOption,
5
+ colorOption,
6
+ noColorOption,
5
7
  quietOption,
6
8
  verboseOption,
7
9
  debugOption
@@ -34,12 +36,20 @@ Output Modes:
34
36
 
35
37
  Set defaults: quire settings set verbose true
36
38
 
39
+ Color Output:
40
+ --no-color Disable colored output
41
+ --color Force colored output (overrides NO_COLOR env var)
42
+
43
+ Respects NO_COLOR environment variable (https://no-color.org/)
44
+ Set default: quire settings set logUseColor false
45
+
37
46
  Paging:
38
47
  --no-pager Disable paging for long output
39
48
  NO_PAGER=1 Disable paging via environment variable
40
49
  PAGER=cat Traditional Unix alternative (passes output through)
41
50
 
42
51
  Environment Variables:
52
+ NO_COLOR Disable colored output (https://no-color.org/)
43
53
  NO_PAGER=1 Disable paging for long output
44
54
  PAGER=<program> Set pager program (default: less). Use PAGER=cat to disable
45
55
  DEBUG=quire:* Enable debug output for all modules
@@ -50,6 +60,8 @@ Examples:
50
60
  $ quire build Build the publication
51
61
  $ quire build --verbose Build with detailed progress
52
62
  $ quire build --debug Build with debug output
63
+ $ quire build --no-color Build without colored output
64
+ $ NO_COLOR=1 quire build Build without colored output
53
65
  $ DEBUG=quire:* quire pdf Generate PDF with debug output
54
66
  `
55
67
 
@@ -66,6 +78,8 @@ program
66
78
  .name('quire')
67
79
  .description('Quire command-line interface')
68
80
  .version(version, '-V, --version', 'output quire version number')
81
+ .addOption(arrayToOption(colorOption))
82
+ .addOption(arrayToOption(noColorOption))
69
83
  .addOption(arrayToOption(quietOption))
70
84
  .addOption(arrayToOption(verboseOption))
71
85
  .addOption(arrayToOption(debugOption))
@@ -88,6 +102,8 @@ program
88
102
  * - --quiet: Suppress progress spinners (for CI/scripts)
89
103
  * - --verbose: Show detailed progress (paths, timing, steps)
90
104
  * - --debug: Enable DEBUG namespace + tool debug modes (for developers)
105
+ * - --no-color: Disable colored output (sets NO_COLOR env var)
106
+ * - --color: Force colored output (overrides NO_COLOR env var)
91
107
  *
92
108
  * These global options are passed through to commands via opts()
93
109
  * and should be merged with command-level options.
@@ -95,6 +111,16 @@ program
95
111
  program.hook('preAction', (thisCommand) => {
96
112
  const opts = thisCommand.opts()
97
113
 
114
+ // Handle --no-color / --color flag
115
+ // Sets NO_COLOR env var for chalk, ora, and logger to read
116
+ if (opts.color === false) {
117
+ process.env.NO_COLOR = '1'
118
+ delete process.env.FORCE_COLOR
119
+ } else if (opts.color === true) {
120
+ delete process.env.NO_COLOR
121
+ process.env.FORCE_COLOR = '1'
122
+ }
123
+
98
124
  // --debug or config.debug enables the quire:* DEBUG namespace for internal logging
99
125
  // CLI flag takes precedence, then config setting
100
126
  if (opts.debug ?? config.get('debug')) {