@thegetty/quire-cli 1.0.0-rc.38 → 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 +14 -1
- package/package.json +3 -1
- package/src/commands/config.js +1 -1
- package/src/commands/help.js +60 -0
- package/src/commands/help.test.js +132 -0
- package/src/errors/help/help-topic-not-found-error.js +22 -0
- package/src/errors/help/index.js +8 -0
- package/src/errors/index.js +6 -0
- package/src/errors/input/index.js +8 -0
- package/src/errors/input/invalid-input-error.js +21 -0
- package/src/helpers/pager.js +59 -0
- package/src/helpers/pager.test.js +85 -0
- package/src/lib/commander/index.js +2 -0
- package/src/lib/commander/options.js +24 -0
- package/src/lib/commander/options.test.js +46 -1
- package/src/lib/help/frontmatter.js +77 -0
- package/src/lib/help/frontmatter.test.js +139 -0
- package/src/lib/help/index.js +135 -0
- package/src/lib/help/index.test.js +160 -0
- package/src/lib/help/topics/configuration.md +77 -0
- package/src/lib/help/topics/debugging.md +69 -0
- package/src/lib/help/topics/epub.md +74 -0
- package/src/lib/help/topics/pdf.md +74 -0
- package/src/lib/help/topics/publishing.md +80 -0
- package/src/lib/help/topics/workflows.md +50 -0
- package/src/lib/logger/index.js +1 -1
- package/src/lib/logger/index.test.js +101 -0
- package/src/main.js +40 -1
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
|
-
*
|
|
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.
|
|
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": {
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
"inquirer": "^9.1.4",
|
|
71
71
|
"js-yaml": "^4.1.0",
|
|
72
72
|
"loglevel": "^1.8.1",
|
|
73
|
+
"marked": "^15.0.12",
|
|
74
|
+
"marked-terminal": "^7.3.0",
|
|
73
75
|
"node-fetch": "^3.3.2",
|
|
74
76
|
"open": "^8.4.0",
|
|
75
77
|
"ora": "^6.1.2",
|
package/src/commands/config.js
CHANGED
|
@@ -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
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Command from '#src/Command.js'
|
|
2
|
+
import { outputWithPaging } from '#helpers/pager.js'
|
|
3
|
+
import { getTopicContent, getTopicList } from '#lib/help/index.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Quire CLI `help` Command
|
|
7
|
+
*
|
|
8
|
+
* Display help for a specific topic or list available topics.
|
|
9
|
+
* Complements Commander's built-in help with extended documentation.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* quire help # List available topics
|
|
13
|
+
* quire help workflows # Show workflows topic
|
|
14
|
+
* quire help --list # List available topics
|
|
15
|
+
*
|
|
16
|
+
* @class HelpCommand
|
|
17
|
+
* @extends {Command}
|
|
18
|
+
*/
|
|
19
|
+
export default class HelpCommand extends Command {
|
|
20
|
+
static definition = {
|
|
21
|
+
name: 'help',
|
|
22
|
+
aliases: ['h'],
|
|
23
|
+
description: 'Display help for a topic or list available topics',
|
|
24
|
+
summary: 'show help for a topic',
|
|
25
|
+
docsLink: 'quire-commands/',
|
|
26
|
+
helpText: `
|
|
27
|
+
Examples:
|
|
28
|
+
quire help List available help topics
|
|
29
|
+
quire help workflows Show common workflow examples
|
|
30
|
+
quire help pdf Show PDF generation guide
|
|
31
|
+
quire help --list List all available topics
|
|
32
|
+
`,
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
args: [
|
|
35
|
+
['[topic]', 'help topic to display']
|
|
36
|
+
],
|
|
37
|
+
options: [
|
|
38
|
+
['--list', 'list all available topics'],
|
|
39
|
+
],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
super(HelpCommand.definition)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async action(topic, options, command) {
|
|
47
|
+
this.debug('called with topic=%s options=%O', topic, options)
|
|
48
|
+
|
|
49
|
+
// List topics if no topic specified or --list flag
|
|
50
|
+
if (!topic || options.list) {
|
|
51
|
+
const list = await getTopicList()
|
|
52
|
+
process.stdout.write(list + '\n')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Display requested topic with paging (throws HelpTopicNotFoundError if missing)
|
|
57
|
+
const content = await getTopicContent(topic)
|
|
58
|
+
await outputWithPaging(content)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import esmock from 'esmock'
|
|
2
|
+
import sinon from 'sinon'
|
|
3
|
+
import test from 'ava'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Help Command Integration Tests
|
|
7
|
+
*
|
|
8
|
+
* Tests the help command behavior with mocked dependencies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
test.serial('help command outputs topic list when called without arguments', async (t) => {
|
|
12
|
+
const sandbox = sinon.createSandbox()
|
|
13
|
+
const stdoutWrite = sandbox.stub(process.stdout, 'write')
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const mockList = 'Available help topics:\n debugging Troubleshooting help'
|
|
17
|
+
|
|
18
|
+
const HelpCommand = await esmock('./help.js', {
|
|
19
|
+
'#lib/help/index.js': {
|
|
20
|
+
getTopicList: sandbox.stub().resolves(mockList),
|
|
21
|
+
getTopicContent: sandbox.stub()
|
|
22
|
+
},
|
|
23
|
+
'#helpers/pager.js': {
|
|
24
|
+
outputWithPaging: sandbox.stub()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const command = new HelpCommand.default()
|
|
29
|
+
await command.action(undefined, {}, {})
|
|
30
|
+
|
|
31
|
+
t.true(stdoutWrite.calledOnce)
|
|
32
|
+
t.true(stdoutWrite.calledWith(mockList + '\n'))
|
|
33
|
+
} finally {
|
|
34
|
+
sandbox.restore()
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test.serial('help command outputs topic list when --list flag is provided', async (t) => {
|
|
39
|
+
const sandbox = sinon.createSandbox()
|
|
40
|
+
const stdoutWrite = sandbox.stub(process.stdout, 'write')
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const mockList = 'Available help topics:\n pdf PDF generation'
|
|
44
|
+
|
|
45
|
+
const HelpCommand = await esmock('./help.js', {
|
|
46
|
+
'#lib/help/index.js': {
|
|
47
|
+
getTopicList: sandbox.stub().resolves(mockList),
|
|
48
|
+
getTopicContent: sandbox.stub()
|
|
49
|
+
},
|
|
50
|
+
'#helpers/pager.js': {
|
|
51
|
+
outputWithPaging: sandbox.stub()
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const command = new HelpCommand.default()
|
|
56
|
+
await command.action('ignored', { list: true }, {})
|
|
57
|
+
|
|
58
|
+
t.true(stdoutWrite.calledOnce)
|
|
59
|
+
t.true(stdoutWrite.calledWith(mockList + '\n'))
|
|
60
|
+
} finally {
|
|
61
|
+
sandbox.restore()
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('help command outputs topic content with paging when topic is specified', async (t) => {
|
|
66
|
+
const sandbox = sinon.createSandbox()
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const mockRendered = 'Rendered workflows content'
|
|
70
|
+
const mockOutputWithPaging = sandbox.stub().resolves()
|
|
71
|
+
|
|
72
|
+
const HelpCommand = await esmock('./help.js', {
|
|
73
|
+
'#lib/help/index.js': {
|
|
74
|
+
getTopicList: sandbox.stub(),
|
|
75
|
+
getTopicContent: sandbox.stub().resolves(mockRendered)
|
|
76
|
+
},
|
|
77
|
+
'#helpers/pager.js': {
|
|
78
|
+
outputWithPaging: mockOutputWithPaging
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const command = new HelpCommand.default()
|
|
83
|
+
await command.action('workflows', {}, {})
|
|
84
|
+
|
|
85
|
+
t.true(mockOutputWithPaging.calledOnce)
|
|
86
|
+
t.true(mockOutputWithPaging.calledWith(mockRendered))
|
|
87
|
+
} finally {
|
|
88
|
+
sandbox.restore()
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('help command propagates HelpTopicNotFoundError from getTopicContent', async (t) => {
|
|
93
|
+
const sandbox = sinon.createSandbox()
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const { default: HelpTopicNotFoundError } = await import('#src/errors/help/help-topic-not-found-error.js')
|
|
97
|
+
|
|
98
|
+
const HelpCommand = await esmock('./help.js', {
|
|
99
|
+
'#lib/help/index.js': {
|
|
100
|
+
getTopicList: sandbox.stub(),
|
|
101
|
+
getTopicContent: sandbox.stub().rejects(new HelpTopicNotFoundError('nonexistent'))
|
|
102
|
+
},
|
|
103
|
+
'#helpers/pager.js': {
|
|
104
|
+
outputWithPaging: sandbox.stub()
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const command = new HelpCommand.default()
|
|
109
|
+
|
|
110
|
+
const error = await t.throwsAsync(
|
|
111
|
+
() => command.action('nonexistent', {}, {}),
|
|
112
|
+
{ name: 'HelpTopicNotFoundError' }
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
t.is(error.code, 'HELP_TOPIC_NOT_FOUND')
|
|
116
|
+
t.is(error.exitCode, 2)
|
|
117
|
+
t.is(error.topic, 'nonexistent')
|
|
118
|
+
} finally {
|
|
119
|
+
sandbox.restore()
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('help command has correct definition', async (t) => {
|
|
124
|
+
const HelpCommand = await import('./help.js')
|
|
125
|
+
const command = new HelpCommand.default()
|
|
126
|
+
|
|
127
|
+
t.is(command.name, 'help')
|
|
128
|
+
t.deepEqual(command.aliases, ['h'])
|
|
129
|
+
t.truthy(command.description)
|
|
130
|
+
t.truthy(command.args)
|
|
131
|
+
t.truthy(command.options)
|
|
132
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import InvalidInputError from '../input/invalid-input-error.js'
|
|
2
|
+
import QuireError from '../quire-error.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when a requested help topic does not exist
|
|
6
|
+
*/
|
|
7
|
+
export default class HelpTopicNotFoundError extends InvalidInputError {
|
|
8
|
+
constructor(topic) {
|
|
9
|
+
super(
|
|
10
|
+
`Unknown help topic: ${topic}`,
|
|
11
|
+
{
|
|
12
|
+
code: 'HELP_TOPIC_NOT_FOUND',
|
|
13
|
+
exitCode: 2,
|
|
14
|
+
inputValue: topic,
|
|
15
|
+
inputName: 'topic',
|
|
16
|
+
suggestion: 'Run "quire help --list" to see available topics',
|
|
17
|
+
docsUrl: `${QuireError.DOCS_BASE}/quire-commands/`
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
this.topic = topic
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/errors/index.js
CHANGED
|
@@ -46,3 +46,9 @@ export {
|
|
|
46
46
|
|
|
47
47
|
// Validation errors (exit code: 4)
|
|
48
48
|
export { default as ValidationError } from './validation/validation-error.js'
|
|
49
|
+
|
|
50
|
+
// Input errors (exit code: 2)
|
|
51
|
+
export { InvalidInputError } from './input/index.js'
|
|
52
|
+
|
|
53
|
+
// Help errors (exit code: 2)
|
|
54
|
+
export { HelpTopicNotFoundError } from './help/index.js'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import QuireError from '../quire-error.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when user input is invalid
|
|
5
|
+
*
|
|
6
|
+
* Generic error for invalid arguments, options, or other user-provided values.
|
|
7
|
+
* Can be extended for specific input validation scenarios.
|
|
8
|
+
*/
|
|
9
|
+
export default class InvalidInputError extends QuireError {
|
|
10
|
+
constructor(message, options = {}) {
|
|
11
|
+
super(message, {
|
|
12
|
+
code: options.code || 'INVALID_INPUT',
|
|
13
|
+
exitCode: options.exitCode ?? 2,
|
|
14
|
+
suggestion: options.suggestion,
|
|
15
|
+
docsUrl: options.docsUrl,
|
|
16
|
+
...options
|
|
17
|
+
})
|
|
18
|
+
this.inputValue = options.inputValue
|
|
19
|
+
this.inputName = options.inputName
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal pager utility
|
|
3
|
+
*
|
|
4
|
+
* Provides paged output for long content in interactive terminals.
|
|
5
|
+
* Paging can be disabled with:
|
|
6
|
+
* - `--no-pager` CLI flag (sets NO_PAGER env var)
|
|
7
|
+
* - `NO_PAGER=1` environment variable
|
|
8
|
+
* - `PAGER=cat` traditional Unix convention (content passes through without paging)
|
|
9
|
+
*
|
|
10
|
+
* @module helpers/pager
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Output content with optional paging for long content
|
|
16
|
+
*
|
|
17
|
+
* Uses system pager (less/more) when:
|
|
18
|
+
* - Output is to an interactive terminal (TTY)
|
|
19
|
+
* - Content exceeds terminal height
|
|
20
|
+
* - Paging is not disabled via NO_PAGER env var
|
|
21
|
+
*
|
|
22
|
+
* The PAGER env var selects the pager program (default: less on Unix, more on
|
|
23
|
+
* Windows). Setting PAGER=cat effectively disables paging as a traditional
|
|
24
|
+
* Unix convention.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} content - Content to output
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
export async function outputWithPaging(content) {
|
|
30
|
+
const noPager = process.env.NO_PAGER
|
|
31
|
+
const lines = content.split('\n').length
|
|
32
|
+
const isTTY = process.stdout.isTTY
|
|
33
|
+
const terminalRows = process.stdout.rows || 24
|
|
34
|
+
|
|
35
|
+
// Use pager if content exceeds terminal height and we're in a TTY
|
|
36
|
+
if (!noPager && isTTY && lines > terminalRows) {
|
|
37
|
+
const pager = process.env.PAGER || (process.platform === 'win32' ? 'more' : 'less')
|
|
38
|
+
const pagerArgs = pager === 'less' ? ['-R'] : [] // -R preserves ANSI colors
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const child = spawn(pager, pagerArgs, {
|
|
42
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
child.stdin.write(content + '\n')
|
|
46
|
+
child.stdin.end()
|
|
47
|
+
|
|
48
|
+
child.on('close', resolve)
|
|
49
|
+
child.on('error', () => {
|
|
50
|
+
// Fallback to direct output if pager fails
|
|
51
|
+
process.stdout.write(content + '\n')
|
|
52
|
+
resolve()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Direct output for non-TTY or short content
|
|
58
|
+
process.stdout.write(content + '\n')
|
|
59
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sinon from 'sinon'
|
|
2
|
+
import test from 'ava'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pager Helper Tests
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
test.beforeEach((t) => {
|
|
9
|
+
t.context.sandbox = sinon.createSandbox()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test.afterEach.always((t) => {
|
|
13
|
+
t.context.sandbox.restore()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test.serial('outputWithPaging() writes directly when not a TTY', async (t) => {
|
|
17
|
+
const { sandbox } = t.context
|
|
18
|
+
const originalIsTTY = process.stdout.isTTY
|
|
19
|
+
const stdoutWrite = sandbox.stub(process.stdout, 'write')
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Simulate non-TTY (piped output)
|
|
23
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true })
|
|
24
|
+
|
|
25
|
+
const { outputWithPaging } = await import('./pager.js')
|
|
26
|
+
await outputWithPaging('Short content')
|
|
27
|
+
|
|
28
|
+
t.true(stdoutWrite.calledWith('Short content\n'))
|
|
29
|
+
} finally {
|
|
30
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test.serial('outputWithPaging() writes directly when content is short', async (t) => {
|
|
35
|
+
const { sandbox } = t.context
|
|
36
|
+
const originalIsTTY = process.stdout.isTTY
|
|
37
|
+
const originalRows = process.stdout.rows
|
|
38
|
+
const stdoutWrite = sandbox.stub(process.stdout, 'write')
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Simulate TTY with 24 rows
|
|
42
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
43
|
+
Object.defineProperty(process.stdout, 'rows', { value: 24, configurable: true })
|
|
44
|
+
|
|
45
|
+
const { outputWithPaging } = await import('./pager.js')
|
|
46
|
+
await outputWithPaging('Line 1\nLine 2\nLine 3')
|
|
47
|
+
|
|
48
|
+
t.true(stdoutWrite.calledWith('Line 1\nLine 2\nLine 3\n'))
|
|
49
|
+
} finally {
|
|
50
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
51
|
+
Object.defineProperty(process.stdout, 'rows', { value: originalRows, configurable: true })
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test.serial('outputWithPaging() writes directly when NO_PAGER is set', async (t) => {
|
|
56
|
+
const { sandbox } = t.context
|
|
57
|
+
const originalIsTTY = process.stdout.isTTY
|
|
58
|
+
const originalRows = process.stdout.rows
|
|
59
|
+
const originalNoPager = process.env.NO_PAGER
|
|
60
|
+
const stdoutWrite = sandbox.stub(process.stdout, 'write')
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Simulate TTY with small terminal (would normally trigger pager)
|
|
64
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
65
|
+
Object.defineProperty(process.stdout, 'rows', { value: 5, configurable: true })
|
|
66
|
+
process.env.NO_PAGER = '1'
|
|
67
|
+
|
|
68
|
+
const { outputWithPaging } = await import('./pager.js')
|
|
69
|
+
|
|
70
|
+
// Content longer than terminal height
|
|
71
|
+
const longContent = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`).join('\n')
|
|
72
|
+
await outputWithPaging(longContent)
|
|
73
|
+
|
|
74
|
+
// Should write directly despite long content, because pager is disabled
|
|
75
|
+
t.true(stdoutWrite.calledWith(longContent + '\n'))
|
|
76
|
+
} finally {
|
|
77
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
78
|
+
Object.defineProperty(process.stdout, 'rows', { value: originalRows, configurable: true })
|
|
79
|
+
if (originalNoPager === undefined) {
|
|
80
|
+
delete process.env.NO_PAGER
|
|
81
|
+
} else {
|
|
82
|
+
process.env.NO_PAGER = originalNoPager
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML frontmatter parsing utilities
|
|
3
|
+
*
|
|
4
|
+
* Extracts and strips YAML frontmatter from markdown content.
|
|
5
|
+
* Frontmatter is delimited by `---` on its own line.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/help/frontmatter
|
|
8
|
+
*/
|
|
9
|
+
import yaml from 'js-yaml'
|
|
10
|
+
|
|
11
|
+
/** Regex to match YAML frontmatter block */
|
|
12
|
+
const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract YAML frontmatter from markdown content
|
|
16
|
+
*
|
|
17
|
+
* @param {string} content - Markdown content with optional frontmatter
|
|
18
|
+
* @returns {Object} Parsed frontmatter data or empty object if none/invalid
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const meta = extractFrontmatter(`---
|
|
22
|
+
* title: My Topic
|
|
23
|
+
* description: A description
|
|
24
|
+
* ---
|
|
25
|
+
* # Content`)
|
|
26
|
+
* // => { title: 'My Topic', description: 'A description' }
|
|
27
|
+
*/
|
|
28
|
+
export function extractFrontmatter(content) {
|
|
29
|
+
const match = content.match(FRONTMATTER_REGEX)
|
|
30
|
+
if (!match) return {}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return yaml.load(match[1]) || {}
|
|
34
|
+
} catch {
|
|
35
|
+
return {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strip YAML frontmatter from markdown content
|
|
41
|
+
*
|
|
42
|
+
* @param {string} content - Markdown content with optional frontmatter
|
|
43
|
+
* @returns {string} Content without frontmatter block
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const body = stripFrontmatter(`---
|
|
47
|
+
* title: My Topic
|
|
48
|
+
* ---
|
|
49
|
+
* # Content`)
|
|
50
|
+
* // => '# Content'
|
|
51
|
+
*/
|
|
52
|
+
export function stripFrontmatter(content) {
|
|
53
|
+
return content.replace(/^---\n[\s\S]*?\n---\n?/, '')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse markdown content with frontmatter
|
|
58
|
+
*
|
|
59
|
+
* Convenience function that returns both metadata and content body.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} content - Markdown content with optional frontmatter
|
|
62
|
+
* @returns {{ data: Object, content: string }} Parsed frontmatter and body
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const { data, content } = parseFrontmatter(`---
|
|
66
|
+
* title: My Topic
|
|
67
|
+
* ---
|
|
68
|
+
* # Content`)
|
|
69
|
+
* // data => { title: 'My Topic' }
|
|
70
|
+
* // content => '# Content'
|
|
71
|
+
*/
|
|
72
|
+
export function parseFrontmatter(content) {
|
|
73
|
+
return {
|
|
74
|
+
data: extractFrontmatter(content),
|
|
75
|
+
content: stripFrontmatter(content)
|
|
76
|
+
}
|
|
77
|
+
}
|