@thegetty/quire-cli 1.0.0-rc.36 → 1.0.0-rc.38
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/package.json +3 -2
- package/src/commands/build.js +3 -4
- package/src/commands/build.test.js +2 -7
- package/src/commands/clean.js +10 -2
- package/src/commands/clean.spec.js +10 -0
- package/src/commands/clean.test.js +122 -0
- package/src/commands/config.js +3 -1
- package/src/commands/config.test.js +42 -1
- package/src/commands/doctor.js +251 -0
- package/src/commands/doctor.spec.js +115 -0
- package/src/commands/doctor.test.js +1409 -0
- package/src/commands/epub.js +9 -2
- package/src/commands/info.js +79 -71
- package/src/commands/info.spec.js +8 -0
- package/src/commands/info.test.js +173 -76
- package/src/commands/pdf.js +10 -3
- package/src/commands/preview.js +8 -2
- package/src/commands/validate.js +39 -7
- package/src/commands/validate.spec.js +8 -0
- package/src/commands/validate.test.js +144 -0
- package/src/lib/11ty/api.js +25 -6
- package/src/lib/11ty/cli.js +17 -5
- package/src/lib/README.md +77 -22
- package/src/lib/conf/README.md +6 -0
- package/src/lib/conf/build-status.js +103 -0
- package/src/lib/conf/build-status.test.js +247 -0
- package/src/lib/conf/defaults.js +14 -0
- package/src/lib/conf/format.js +1 -1
- package/src/lib/conf/schema.js +46 -1
- package/src/lib/constants.js +28 -0
- package/src/lib/doctor/README.md +667 -0
- package/src/lib/doctor/checks/environment/cli-version.js +62 -0
- package/src/lib/doctor/checks/environment/cli-version.test.js +132 -0
- package/src/lib/doctor/checks/environment/git-available.js +77 -0
- package/src/lib/doctor/checks/environment/git-available.test.js +52 -0
- package/src/lib/doctor/checks/environment/index.js +13 -0
- package/src/lib/doctor/checks/environment/node-version.js +66 -0
- package/src/lib/doctor/checks/environment/node-version.test.js +17 -0
- package/src/lib/doctor/checks/environment/npm-available.js +66 -0
- package/src/lib/doctor/checks/environment/npm-available.test.js +52 -0
- package/src/lib/doctor/checks/environment/os-info.js +48 -0
- package/src/lib/doctor/checks/environment/os-info.test.js +81 -0
- package/src/lib/doctor/checks/environment/runtime-info.js +51 -0
- package/src/lib/doctor/checks/environment/runtime-info.test.js +132 -0
- package/src/lib/doctor/checks/outputs/epub-output.js +119 -0
- package/src/lib/doctor/checks/outputs/epub-output.test.js +277 -0
- package/src/lib/doctor/checks/outputs/index.js +10 -0
- package/src/lib/doctor/checks/outputs/pdf-output.js +144 -0
- package/src/lib/doctor/checks/outputs/pdf-output.test.js +377 -0
- package/src/lib/doctor/checks/outputs/stale-build.js +122 -0
- package/src/lib/doctor/checks/outputs/stale-build.test.js +282 -0
- package/src/lib/doctor/checks/project/data-files.js +56 -0
- package/src/lib/doctor/checks/project/data-files.test.js +125 -0
- package/src/lib/doctor/checks/project/dependencies.js +53 -0
- package/src/lib/doctor/checks/project/dependencies.test.js +71 -0
- package/src/lib/doctor/checks/project/index.js +11 -0
- package/src/lib/doctor/checks/project/quire-11ty.js +98 -0
- package/src/lib/doctor/checks/project/quire-11ty.test.js +170 -0
- package/src/lib/doctor/checks/project/quire-project.js +38 -0
- package/src/lib/doctor/checks/project/quire-project.test.js +47 -0
- package/src/lib/doctor/checks/tools/index.js +10 -0
- package/src/lib/doctor/checks/tools/pandoc-available.js +82 -0
- package/src/lib/doctor/checks/tools/pandoc-available.test.js +73 -0
- package/src/lib/doctor/checks/tools/prince-available.js +81 -0
- package/src/lib/doctor/checks/tools/prince-available.test.js +73 -0
- package/src/lib/doctor/constants.js +39 -0
- package/src/lib/doctor/formatDuration.js +108 -0
- package/src/lib/doctor/formatDuration.test.js +76 -0
- package/src/lib/doctor/formatters/human.js +257 -0
- package/src/lib/doctor/formatters/human.test.js +463 -0
- package/src/lib/doctor/formatters/index.js +8 -0
- package/src/lib/doctor/formatters/json.js +78 -0
- package/src/lib/doctor/formatters/json.test.js +174 -0
- package/src/lib/doctor/formatters/shared.js +129 -0
- package/src/lib/doctor/formatters/shared.test.js +194 -0
- package/src/lib/doctor/index.js +271 -0
- package/src/lib/doctor/index.test.js +797 -0
- package/src/lib/logger/debug.js +43 -1
- package/src/lib/logger/debug.test.js +128 -0
- package/src/lib/platform.js +95 -0
- package/src/lib/project/build.js +44 -25
- package/src/lib/project/build.test.js +30 -8
- package/src/lib/project/detect.js +16 -2
- package/src/lib/project/index.js +18 -2
- package/src/lib/project/output-paths.js +87 -0
- package/src/lib/project/output-paths.test.js +66 -0
- package/src/lib/project/paths.js +48 -0
- package/src/main.js +24 -2
- package/src/packageConfig.js +17 -0
- package/src/validators/validate-data-files.js +154 -0
- package/src/validators/validate-data-files.test.js +217 -0
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.38",
|
|
5
5
|
"author": "Getty Digital",
|
|
6
6
|
"license": "SEE LICENSE IN https://github.com/thegetty/quire/blob/main/LICENSE",
|
|
7
7
|
"bugs": {
|
|
@@ -78,7 +78,8 @@
|
|
|
78
78
|
"read-package-up": "^11.0.0",
|
|
79
79
|
"semver": "^7.3.8",
|
|
80
80
|
"update-notifier": "^7.3.1",
|
|
81
|
-
"which": "^2.0.2"
|
|
81
|
+
"which": "^2.0.2",
|
|
82
|
+
"wrap-ansi": "^8.1.0"
|
|
82
83
|
},
|
|
83
84
|
"devDependencies": {
|
|
84
85
|
"ava": "^6.2.0",
|
package/src/commands/build.js
CHANGED
|
@@ -4,6 +4,7 @@ import { withOutputModes } from '#lib/commander/index.js'
|
|
|
4
4
|
import { api, cli } from '#lib/11ty/index.js'
|
|
5
5
|
import paths from '#lib/project/index.js'
|
|
6
6
|
import { clean } from '#helpers/clean.js'
|
|
7
|
+
import { recordStatus } from '#lib/conf/build-status.js'
|
|
7
8
|
import reporter from '#lib/reporter/index.js'
|
|
8
9
|
import testcwd from '#helpers/test-cwd.js'
|
|
9
10
|
|
|
@@ -49,8 +50,6 @@ Note: Run before "quire pdf" or "quire epub" commands.
|
|
|
49
50
|
// Configure reporter for this command
|
|
50
51
|
reporter.configure({ quiet: options.quiet, verbose: options.verbose })
|
|
51
52
|
|
|
52
|
-
reporter.start('Building site...', { showElapsed: true })
|
|
53
|
-
|
|
54
53
|
try {
|
|
55
54
|
if (options['11ty'] === 'api') {
|
|
56
55
|
this.debug('running eleventy using lib/11ty api')
|
|
@@ -59,9 +58,9 @@ Note: Run before "quire pdf" or "quire epub" commands.
|
|
|
59
58
|
this.debug('running eleventy using lib/11ty cli')
|
|
60
59
|
await cli.build(options)
|
|
61
60
|
}
|
|
62
|
-
|
|
61
|
+
recordStatus(paths.getProjectRoot(), 'build', 'ok')
|
|
63
62
|
} catch (error) {
|
|
64
|
-
|
|
63
|
+
recordStatus(paths.getProjectRoot(), 'build', 'failed')
|
|
65
64
|
throw error
|
|
66
65
|
}
|
|
67
66
|
}
|
|
@@ -108,8 +108,6 @@ test('build command should call eleventy CLI with default options', async (t) =>
|
|
|
108
108
|
t.true(mockClean.called, 'clean should be called in preAction')
|
|
109
109
|
t.false(mockEleventyApi.build.called, 'eleventy API build should not be called')
|
|
110
110
|
t.true(mockEleventyCli.build.called, 'eleventy CLI build should be called')
|
|
111
|
-
t.true(mockReporter.start.called, 'reporter.start should be called')
|
|
112
|
-
t.true(mockReporter.succeed.called, 'reporter.succeed should be called')
|
|
113
111
|
})
|
|
114
112
|
|
|
115
113
|
test('build command should call eleventy API when 11ty option is "api"', async (t) => {
|
|
@@ -282,7 +280,7 @@ test('build command should pass options to eleventy build', async (t) => {
|
|
|
282
280
|
t.true(mockEleventyCli.build.calledWith(options), 'eleventy CLI build should be called with options')
|
|
283
281
|
})
|
|
284
282
|
|
|
285
|
-
test('build command should
|
|
283
|
+
test('build command should propagate errors from eleventy build', async (t) => {
|
|
286
284
|
const { sandbox, fs, mockLogger, mockReporter } = t.context
|
|
287
285
|
|
|
288
286
|
const buildError = new Error('Build failed')
|
|
@@ -334,11 +332,8 @@ test('build command should call reporter.fail when build fails', async (t) => {
|
|
|
334
332
|
command.name = sandbox.stub().returns('build')
|
|
335
333
|
|
|
336
334
|
// Run action and expect it to throw
|
|
335
|
+
// Nota bene: reporter.start/succeed/fail are called by the lib layer, not the command
|
|
337
336
|
await t.throwsAsync(() => command.action({ '11ty': 'api' }, command), { message: 'Build failed' })
|
|
338
|
-
|
|
339
|
-
t.true(mockReporter.start.called, 'reporter.start should be called')
|
|
340
|
-
t.true(mockReporter.fail.called, 'reporter.fail should be called on error')
|
|
341
|
-
t.false(mockReporter.succeed.called, 'reporter.succeed should not be called on error')
|
|
342
337
|
})
|
|
343
338
|
|
|
344
339
|
test('build command should configure reporter with quiet option', async (t) => {
|
package/src/commands/clean.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Command from '#src/Command.js'
|
|
2
2
|
import { withOutputModes } from '#lib/commander/index.js'
|
|
3
3
|
import { clean } from '#helpers/clean.js'
|
|
4
|
+
import { clearStatus } from '#lib/conf/build-status.js'
|
|
4
5
|
import paths from '#lib/project/index.js'
|
|
5
6
|
import testcwd from '#helpers/test-cwd.js'
|
|
6
7
|
|
|
@@ -31,6 +32,7 @@ Examples:
|
|
|
31
32
|
options: [
|
|
32
33
|
[ '-d, --dry-run', 'show paths to be cleaned without deleting files' ],
|
|
33
34
|
[ '--dryrun', 'alias for --dry-run', { hidden: true, implies: { dryRun: true } } ],
|
|
35
|
+
[ '--status', 'also clear stored build status for this project' ],
|
|
34
36
|
],
|
|
35
37
|
})
|
|
36
38
|
|
|
@@ -41,10 +43,16 @@ Examples:
|
|
|
41
43
|
async action(options, command) {
|
|
42
44
|
this.debug('called with options %O', options)
|
|
43
45
|
|
|
44
|
-
const
|
|
46
|
+
const projectRoot = paths.getProjectRoot()
|
|
47
|
+
const deletedPaths = await clean(projectRoot, paths.toObject(), options)
|
|
48
|
+
|
|
49
|
+
if (options.status && !options.dryRun) {
|
|
50
|
+
clearStatus(projectRoot)
|
|
51
|
+
this.logger.info('Cleared build status for this project')
|
|
52
|
+
}
|
|
45
53
|
|
|
46
54
|
if (!deletedPaths || !deletedPaths.length) {
|
|
47
|
-
this.logger.info('No files to delete')
|
|
55
|
+
if (!options.status) this.logger.info('No files to delete')
|
|
48
56
|
return
|
|
49
57
|
}
|
|
50
58
|
|
|
@@ -42,6 +42,7 @@ test('registered command has correct options', (t) => {
|
|
|
42
42
|
// Get all options
|
|
43
43
|
const dryRunOption = command.options.find((opt) => opt.long === '--dry-run')
|
|
44
44
|
const dryrunAlias = command.options.find((opt) => opt.long === '--dryrun')
|
|
45
|
+
const statusOption = command.options.find((opt) => opt.long === '--status')
|
|
45
46
|
const progressOption = command.options.find((opt) => opt.long === '--progress')
|
|
46
47
|
const quietOption = command.options.find((opt) => opt.long === '--quiet')
|
|
47
48
|
const verboseOption = command.options.find((opt) => opt.long === '--verbose')
|
|
@@ -50,6 +51,7 @@ test('registered command has correct options', (t) => {
|
|
|
50
51
|
// Verify all options exist
|
|
51
52
|
t.truthy(dryRunOption, '--dry-run option should exist')
|
|
52
53
|
t.truthy(dryrunAlias, '--dryrun alias should exist')
|
|
54
|
+
t.truthy(statusOption, '--status option should exist')
|
|
53
55
|
t.truthy(progressOption, '--progress option should exist')
|
|
54
56
|
t.truthy(quietOption, '--quiet option should exist')
|
|
55
57
|
t.truthy(verboseOption, '--verbose option should exist')
|
|
@@ -58,6 +60,7 @@ test('registered command has correct options', (t) => {
|
|
|
58
60
|
// Verify they are Option instances
|
|
59
61
|
t.true(dryRunOption instanceof Option, '--dry-run should be Option instance')
|
|
60
62
|
t.true(dryrunAlias instanceof Option, '--dryrun should be Option instance')
|
|
63
|
+
t.true(statusOption instanceof Option, '--status should be Option instance')
|
|
61
64
|
t.true(progressOption instanceof Option, '--progress should be Option instance')
|
|
62
65
|
t.true(quietOption instanceof Option, '--quiet should be Option instance')
|
|
63
66
|
t.true(verboseOption instanceof Option, '--verbose should be Option instance')
|
|
@@ -73,6 +76,12 @@ test('registered command has correct options', (t) => {
|
|
|
73
76
|
t.is(dryrunAlias.long, '--dryrun')
|
|
74
77
|
t.true(dryrunAlias.hidden, '--dryrun should be hidden from help')
|
|
75
78
|
|
|
79
|
+
// --status clears stored build status
|
|
80
|
+
t.is(statusOption.long, '--status')
|
|
81
|
+
t.falsy(statusOption.short, '--status should not have a short flag')
|
|
82
|
+
t.truthy(statusOption.description)
|
|
83
|
+
t.false(statusOption.required, '--status should not require a value')
|
|
84
|
+
|
|
76
85
|
// --progress is a hidden alias for --verbose
|
|
77
86
|
t.is(progressOption.long, '--progress')
|
|
78
87
|
t.truthy(progressOption.description)
|
|
@@ -101,6 +110,7 @@ test('command options are accessible via public API', (t) => {
|
|
|
101
110
|
|
|
102
111
|
t.true(optionNames.includes('--dry-run'))
|
|
103
112
|
t.true(optionNames.includes('--dryrun'))
|
|
113
|
+
t.true(optionNames.includes('--status'))
|
|
104
114
|
t.true(optionNames.includes('--progress'))
|
|
105
115
|
t.true(optionNames.includes('--quiet'))
|
|
106
116
|
t.true(optionNames.includes('--verbose'))
|
|
@@ -258,3 +258,125 @@ test('clean command should pass all options to clean helper', async (t) => {
|
|
|
258
258
|
|
|
259
259
|
t.true(mockClean.calledWith('/project', { output: '_site' }, options), 'clean should receive all options')
|
|
260
260
|
})
|
|
261
|
+
|
|
262
|
+
test('clean --status calls clearStatus for the project', async (t) => {
|
|
263
|
+
const { sandbox, fs, mockLogger } = t.context
|
|
264
|
+
|
|
265
|
+
const mockClean = sandbox.stub().resolves(['/project/_site'])
|
|
266
|
+
const mockTestcwd = sandbox.stub()
|
|
267
|
+
const mockClearStatus = sandbox.stub()
|
|
268
|
+
|
|
269
|
+
const CleanCommand = await esmock('./clean.js', {
|
|
270
|
+
'#helpers/clean.js': {
|
|
271
|
+
clean: mockClean
|
|
272
|
+
},
|
|
273
|
+
'#lib/conf/build-status.js': {
|
|
274
|
+
clearStatus: mockClearStatus
|
|
275
|
+
},
|
|
276
|
+
'#lib/project/index.js': {
|
|
277
|
+
default: {
|
|
278
|
+
getProjectRoot: () => '/project',
|
|
279
|
+
toObject: () => ({ output: '_site' })
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
'#helpers/test-cwd.js': {
|
|
283
|
+
default: mockTestcwd
|
|
284
|
+
},
|
|
285
|
+
'#lib/logger/index.js': {
|
|
286
|
+
logger: mockLogger
|
|
287
|
+
},
|
|
288
|
+
'fs-extra': fs
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const command = new CleanCommand()
|
|
292
|
+
command.name = sandbox.stub().returns('clean')
|
|
293
|
+
command.logger = mockLogger
|
|
294
|
+
command.debug = sandbox.stub()
|
|
295
|
+
|
|
296
|
+
await command.action({ status: true }, command)
|
|
297
|
+
|
|
298
|
+
t.true(mockClearStatus.calledOnce, 'clearStatus should be called once')
|
|
299
|
+
t.true(mockClearStatus.calledWith('/project'), 'clearStatus should be called with project root')
|
|
300
|
+
|
|
301
|
+
const calls = mockLogger.info.args.map((a) => a[0])
|
|
302
|
+
t.true(calls.some((c) => c.includes('Cleared build status')), 'should log status cleared message')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('clean --status does not call clearStatus during dry-run', async (t) => {
|
|
306
|
+
const { sandbox, fs, mockLogger } = t.context
|
|
307
|
+
|
|
308
|
+
const mockClean = sandbox.stub().resolves(['/project/_site'])
|
|
309
|
+
const mockTestcwd = sandbox.stub()
|
|
310
|
+
const mockClearStatus = sandbox.stub()
|
|
311
|
+
|
|
312
|
+
const CleanCommand = await esmock('./clean.js', {
|
|
313
|
+
'#helpers/clean.js': {
|
|
314
|
+
clean: mockClean
|
|
315
|
+
},
|
|
316
|
+
'#lib/conf/build-status.js': {
|
|
317
|
+
clearStatus: mockClearStatus
|
|
318
|
+
},
|
|
319
|
+
'#lib/project/index.js': {
|
|
320
|
+
default: {
|
|
321
|
+
getProjectRoot: () => '/project',
|
|
322
|
+
toObject: () => ({ output: '_site' })
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
'#helpers/test-cwd.js': {
|
|
326
|
+
default: mockTestcwd
|
|
327
|
+
},
|
|
328
|
+
'#lib/logger/index.js': {
|
|
329
|
+
logger: mockLogger
|
|
330
|
+
},
|
|
331
|
+
'fs-extra': fs
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const command = new CleanCommand()
|
|
335
|
+
command.name = sandbox.stub().returns('clean')
|
|
336
|
+
command.logger = mockLogger
|
|
337
|
+
command.debug = sandbox.stub()
|
|
338
|
+
|
|
339
|
+
await command.action({ status: true, dryRun: true }, command)
|
|
340
|
+
|
|
341
|
+
t.false(mockClearStatus.called, 'clearStatus should not be called during dry-run')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('clean --status with no files still clears status', async (t) => {
|
|
345
|
+
const { sandbox, fs, mockLogger } = t.context
|
|
346
|
+
|
|
347
|
+
const mockClean = sandbox.stub().resolves([])
|
|
348
|
+
const mockTestcwd = sandbox.stub()
|
|
349
|
+
const mockClearStatus = sandbox.stub()
|
|
350
|
+
|
|
351
|
+
const CleanCommand = await esmock('./clean.js', {
|
|
352
|
+
'#helpers/clean.js': {
|
|
353
|
+
clean: mockClean
|
|
354
|
+
},
|
|
355
|
+
'#lib/conf/build-status.js': {
|
|
356
|
+
clearStatus: mockClearStatus
|
|
357
|
+
},
|
|
358
|
+
'#lib/project/index.js': {
|
|
359
|
+
default: {
|
|
360
|
+
getProjectRoot: () => '/project',
|
|
361
|
+
toObject: () => ({ output: '_site' })
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
'#helpers/test-cwd.js': {
|
|
365
|
+
default: mockTestcwd
|
|
366
|
+
},
|
|
367
|
+
'#lib/logger/index.js': {
|
|
368
|
+
logger: mockLogger
|
|
369
|
+
},
|
|
370
|
+
'fs-extra': fs
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const command = new CleanCommand()
|
|
374
|
+
command.name = sandbox.stub().returns('clean')
|
|
375
|
+
command.logger = mockLogger
|
|
376
|
+
command.debug = sandbox.stub()
|
|
377
|
+
|
|
378
|
+
await command.action({ status: true }, command)
|
|
379
|
+
|
|
380
|
+
t.true(mockClearStatus.calledOnce, 'clearStatus should be called even when no files deleted')
|
|
381
|
+
t.true(mockClearStatus.calledWith('/project'), 'clearStatus should be called with project root')
|
|
382
|
+
})
|
package/src/commands/config.js
CHANGED
|
@@ -194,7 +194,9 @@ Examples:
|
|
|
194
194
|
const store = options.debug
|
|
195
195
|
? this.config.store
|
|
196
196
|
: Object.fromEntries(
|
|
197
|
-
Object.entries(this.config.store).filter(([k]) =>
|
|
197
|
+
Object.entries(this.config.store).filter(([k]) =>
|
|
198
|
+
!k.startsWith('__internal__') && k !== 'projects'
|
|
199
|
+
)
|
|
198
200
|
)
|
|
199
201
|
console.log(JSON.stringify(store, null, 2))
|
|
200
202
|
return
|
|
@@ -28,7 +28,8 @@ test.beforeEach((t) => {
|
|
|
28
28
|
logShowLevel: false,
|
|
29
29
|
projectTemplate: 'https://github.com/thegetty/quire-starter-default',
|
|
30
30
|
quireVersion: '1.0.0',
|
|
31
|
-
__internal__secretKey: 'hidden-value'
|
|
31
|
+
__internal__secretKey: 'hidden-value',
|
|
32
|
+
projects: { abc123: { projectPath: '/test', buildStatus: { build: { status: 'ok', timestamp: 1000 } } } }
|
|
32
33
|
},
|
|
33
34
|
get: (key) => t.context.mockConfig.store[key],
|
|
34
35
|
set: t.context.sandbox.stub(),
|
|
@@ -115,6 +116,46 @@ test('conf command should show internal config values with debug flag', async (t
|
|
|
115
116
|
t.true(output.includes('__internal__secretKey: "hidden-value"'))
|
|
116
117
|
})
|
|
117
118
|
|
|
119
|
+
test('conf command should hide projects by default', async (t) => {
|
|
120
|
+
const { sandbox, mockLogger, mockConfig } = t.context
|
|
121
|
+
|
|
122
|
+
const { default: ConfCommand } = await esmock('./config.js', {}, {
|
|
123
|
+
'#lib/logger/index.js': {
|
|
124
|
+
default: () => mockLogger
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const command = new ConfCommand()
|
|
129
|
+
command.config = mockConfig
|
|
130
|
+
command.logger = mockLogger
|
|
131
|
+
command.debug = sandbox.stub()
|
|
132
|
+
|
|
133
|
+
await command.action(undefined, undefined, undefined, {})
|
|
134
|
+
|
|
135
|
+
const output = mockLogger.info.firstCall.args[0]
|
|
136
|
+
t.false(output.includes('projects'))
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('conf command should show projects with debug flag', async (t) => {
|
|
140
|
+
const { sandbox, mockLogger, mockConfig } = t.context
|
|
141
|
+
|
|
142
|
+
const { default: ConfCommand } = await esmock('./config.js', {}, {
|
|
143
|
+
'#lib/logger/index.js': {
|
|
144
|
+
default: () => mockLogger
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const command = new ConfCommand()
|
|
149
|
+
command.config = mockConfig
|
|
150
|
+
command.logger = mockLogger
|
|
151
|
+
command.debug = sandbox.stub()
|
|
152
|
+
|
|
153
|
+
await command.action(undefined, undefined, undefined, { debug: true })
|
|
154
|
+
|
|
155
|
+
const output = mockLogger.info.firstCall.args[0]
|
|
156
|
+
t.true(output.includes('projects'))
|
|
157
|
+
})
|
|
158
|
+
|
|
118
159
|
// =============================================================================
|
|
119
160
|
// Get operation
|
|
120
161
|
// =============================================================================
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import Command from '#src/Command.js'
|
|
4
|
+
import { withOutputModes } from '#lib/commander/index.js'
|
|
5
|
+
import { clearStatus } from '#lib/conf/build-status.js'
|
|
6
|
+
import { runAllChecksWithSections, checkSections, SECTION_NAMES, CHECK_IDS } from '#lib/doctor/index.js'
|
|
7
|
+
import { formatHuman } from '#lib/doctor/formatters/human.js'
|
|
8
|
+
import { formatJson } from '#lib/doctor/formatters/json.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Map section name to its check IDs
|
|
12
|
+
*/
|
|
13
|
+
const SECTION_CHECK_MAP = Object.fromEntries(
|
|
14
|
+
checkSections.map((s) => [s.name.toLowerCase(), s.checks.map((c) => c.id)])
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Quire CLI `doctor` Command
|
|
19
|
+
*
|
|
20
|
+
* Diagnose common issues with the Quire environment and project setup.
|
|
21
|
+
*
|
|
22
|
+
* @class DoctorCommand
|
|
23
|
+
* @extends {Command}
|
|
24
|
+
*/
|
|
25
|
+
export default class DoctorCommand extends Command {
|
|
26
|
+
static definition = withOutputModes({
|
|
27
|
+
name: 'doctor',
|
|
28
|
+
aliases: ['checkup', 'check', 'diagnostic', 'health'],
|
|
29
|
+
description: 'Diagnose common issues with your Quire setup',
|
|
30
|
+
summary: 'check environment and project health',
|
|
31
|
+
docsLink: 'quire-commands/#troubleshooting',
|
|
32
|
+
helpText: `
|
|
33
|
+
Runs diagnostic checks organized into four sections:
|
|
34
|
+
• Environment: os, cli, node, runtime, npm, git
|
|
35
|
+
• Tools: prince, pandoc
|
|
36
|
+
• Project: project, deps, 11ty, data
|
|
37
|
+
• Outputs: build, pdf, epub
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
quire doctor Run all diagnostic checks
|
|
41
|
+
quire doctor all Run all checks (same as no argument)
|
|
42
|
+
quire doctor environment Check environment section only
|
|
43
|
+
quire doctor node Check Node.js version only
|
|
44
|
+
quire doctor node git Check multiple items
|
|
45
|
+
quire doctor --errors Show only failed checks
|
|
46
|
+
quire doctor --warnings Show only warnings
|
|
47
|
+
quire doctor --verbose Show additional details (paths, versions)
|
|
48
|
+
quire doctor --json Output results as JSON (to stdout)
|
|
49
|
+
quire doctor --json report.json Save JSON results to file
|
|
50
|
+
quire doctor --reset Clear stored build status
|
|
51
|
+
quire checkup Alias for doctor command
|
|
52
|
+
|
|
53
|
+
CI/Scripting:
|
|
54
|
+
quire doctor --quiet Exit code only (0=success, 1=failure)
|
|
55
|
+
quire doctor --quiet --json out.json Save JSON report silently
|
|
56
|
+
`,
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
args: [
|
|
59
|
+
[
|
|
60
|
+
'[checks...]',
|
|
61
|
+
`run specific check(s): all, ${SECTION_NAMES.join(', ')}, or ${CHECK_IDS.join(', ')}`,
|
|
62
|
+
],
|
|
63
|
+
],
|
|
64
|
+
options: [
|
|
65
|
+
['-e, --errors', 'show only failed checks'],
|
|
66
|
+
['-w, --warnings', 'show only warnings'],
|
|
67
|
+
['--json [file]', 'output results as JSON (to standard out or a file)'],
|
|
68
|
+
['--reset', 'clear stored build status for the current project'],
|
|
69
|
+
],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
constructor() {
|
|
73
|
+
super(DoctorCommand.definition)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async action(checks, options, command) {
|
|
77
|
+
this.debug('called with checks=%O options=%O', checks, options)
|
|
78
|
+
|
|
79
|
+
// --reset: clear stored build status and exit without running checks
|
|
80
|
+
if (options.reset) {
|
|
81
|
+
clearStatus(process.cwd())
|
|
82
|
+
if (!options.quiet) {
|
|
83
|
+
this.logger.info('Build status cleared for this project')
|
|
84
|
+
}
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Parse variadic checks argument into sections and individual checks
|
|
89
|
+
const filterOptions = { sections: null, checks: null }
|
|
90
|
+
let label = 'diagnostic checks'
|
|
91
|
+
|
|
92
|
+
if (checks.length > 0) {
|
|
93
|
+
const values = checks.map((v) => v.toLowerCase())
|
|
94
|
+
|
|
95
|
+
// "all" means no filtering
|
|
96
|
+
if (!values.includes('all')) {
|
|
97
|
+
// Separate section names from individual check IDs
|
|
98
|
+
const sectionValues = values.filter((v) => SECTION_NAMES.includes(v))
|
|
99
|
+
const checkValues = values.filter((v) => CHECK_IDS.includes(v))
|
|
100
|
+
|
|
101
|
+
// If we have section names, use them as section filter
|
|
102
|
+
if (sectionValues.length > 0 && checkValues.length === 0) {
|
|
103
|
+
filterOptions.sections = sectionValues
|
|
104
|
+
label = sectionValues.length === 1
|
|
105
|
+
? `${sectionValues[0]} checks`
|
|
106
|
+
: `${sectionValues.join(', ')} checks`
|
|
107
|
+
} else if (checkValues.length > 0) {
|
|
108
|
+
// If we have individual checks (with or without sections),
|
|
109
|
+
// expand sections to their check IDs and combine
|
|
110
|
+
const expandedChecks = new Set(checkValues)
|
|
111
|
+
for (const section of sectionValues) {
|
|
112
|
+
const sectionCheckIds = SECTION_CHECK_MAP[section] || []
|
|
113
|
+
sectionCheckIds.forEach((id) => expandedChecks.add(id))
|
|
114
|
+
}
|
|
115
|
+
filterOptions.checks = [...expandedChecks]
|
|
116
|
+
label = filterOptions.checks.length === 1
|
|
117
|
+
? `${filterOptions.checks[0]} check`
|
|
118
|
+
: `${filterOptions.checks.join(', ')} checks`
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const sections = await runAllChecksWithSections(filterOptions)
|
|
124
|
+
|
|
125
|
+
// JSON output mode (options.json is true for flag-only, or string for file path)
|
|
126
|
+
if (options.json) {
|
|
127
|
+
this.outputJson(sections, options)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Quiet mode without JSON: just exit with appropriate code
|
|
132
|
+
if (options.quiet) {
|
|
133
|
+
this.outputQuiet(sections)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Human-readable output
|
|
138
|
+
this.outputHuman(sections, options, label)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Quiet mode: no output, just exit code
|
|
143
|
+
* @param {Array} sections - Check results organized by section
|
|
144
|
+
*/
|
|
145
|
+
outputQuiet(sections) {
|
|
146
|
+
const { exitCode } = formatJson(sections, {})
|
|
147
|
+
if (exitCode !== 0) {
|
|
148
|
+
process.exitCode = exitCode
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Output results as JSON for programmatic consumption
|
|
154
|
+
* @param {Array} sections - Check results organized by section
|
|
155
|
+
* @param {Object} options - Command options (options.json is true or filename string)
|
|
156
|
+
*/
|
|
157
|
+
outputJson(sections, options) {
|
|
158
|
+
const { json, exitCode } = formatJson(sections, {
|
|
159
|
+
errors: options.errors,
|
|
160
|
+
warnings: options.warnings,
|
|
161
|
+
verbose: options.verbose,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Write to file (if string path provided) or stdout (if just --json flag)
|
|
165
|
+
if (typeof options.json === 'string') {
|
|
166
|
+
const outputPath = path.resolve(options.json)
|
|
167
|
+
fs.writeFileSync(outputPath, json + '\n')
|
|
168
|
+
if (!options.quiet) {
|
|
169
|
+
this.logger.info(`Results written to ${outputPath}`)
|
|
170
|
+
}
|
|
171
|
+
} else if (!options.quiet) {
|
|
172
|
+
// Write to stdout (bypassing logger formatting) unless quiet mode
|
|
173
|
+
console.log(json)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Set exit code if any checks failed
|
|
177
|
+
if (exitCode !== 0) {
|
|
178
|
+
process.exitCode = exitCode
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Map log level to console method for direct output
|
|
184
|
+
*
|
|
185
|
+
* Nota bene: doctor output bypasses the logger to avoid the `[quire]`
|
|
186
|
+
* prefix on every line. The formatter already handles its own formatting
|
|
187
|
+
* (icons, indentation, section headers). Using console methods directly
|
|
188
|
+
* routes output to the correct output stream (stdout for info, stderr for
|
|
189
|
+
* warn/error) without logger prefix and level labels.
|
|
190
|
+
*/
|
|
191
|
+
static #consoleMethods = {
|
|
192
|
+
info: console.log,
|
|
193
|
+
warn: console.warn,
|
|
194
|
+
error: console.error,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Output results in human-readable format
|
|
199
|
+
* @param {Array} sections - Check results organized by section
|
|
200
|
+
* @param {Object} options - Command options
|
|
201
|
+
* @param {string} label - Description of checks being run
|
|
202
|
+
*/
|
|
203
|
+
outputHuman(sections, options, label) {
|
|
204
|
+
const { lines, summary, key, exitCode, isEmpty } = formatHuman(sections, {
|
|
205
|
+
errors: options.errors,
|
|
206
|
+
warnings: options.warnings,
|
|
207
|
+
verbose: options.verbose,
|
|
208
|
+
label,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Handle case where filters excluded all results
|
|
212
|
+
if (isEmpty) {
|
|
213
|
+
for (const { text, level } of lines) {
|
|
214
|
+
this.#log(level, text)
|
|
215
|
+
}
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Output all formatted lines routed by log level
|
|
220
|
+
for (const { text, level } of lines) {
|
|
221
|
+
this.#log(level, text)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Output summary
|
|
225
|
+
if (summary) {
|
|
226
|
+
this.#log(summary.level, summary.text)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Output symbol key
|
|
230
|
+
if (key) {
|
|
231
|
+
this.#log('info', '')
|
|
232
|
+
this.#log(key.level, key.text)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Set exit code if any checks failed
|
|
236
|
+
if (exitCode !== 0) {
|
|
237
|
+
process.exitCode = exitCode
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Write a line to the console, routed by log level
|
|
243
|
+
*
|
|
244
|
+
* @param {string} level - Log level (info, warn, error)
|
|
245
|
+
* @param {string} text - Text to output
|
|
246
|
+
*/
|
|
247
|
+
#log(level, text) {
|
|
248
|
+
const method = DoctorCommand.#consoleMethods[level] || console.log
|
|
249
|
+
method(text)
|
|
250
|
+
}
|
|
251
|
+
}
|