@thegetty/quire-cli 1.0.0-rc.45 → 1.0.0-rc.46

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.
Files changed (245) hide show
  1. package/CHANGELOG.md +0 -36
  2. package/README.md +0 -9
  3. package/bin/cli.js +1 -46
  4. package/package.json +10 -27
  5. package/src/Command.js +6 -26
  6. package/src/commands/README.md +122 -213
  7. package/src/commands/build.js +37 -45
  8. package/src/commands/clean.js +19 -33
  9. package/src/commands/conf.js +43 -0
  10. package/src/commands/create.js +14 -42
  11. package/src/commands/epub.js +31 -67
  12. package/src/commands/index.js +2 -9
  13. package/src/commands/info.js +86 -88
  14. package/src/commands/pdf.js +82 -63
  15. package/src/commands/preview.js +31 -32
  16. package/src/commands/validate.js +20 -64
  17. package/src/commands/version.js +43 -0
  18. package/src/errors/validation/validation-error.js +11 -20
  19. package/src/helpers/clean.js +1 -1
  20. package/src/helpers/is-quire.js +31 -0
  21. package/src/helpers/test-cwd.js +6 -5
  22. package/src/helpers/which.js +4 -10
  23. package/src/lib/11ty/README.md +19 -135
  24. package/src/lib/11ty/api.js +93 -235
  25. package/src/lib/11ty/cli.js +37 -112
  26. package/src/lib/11ty/index.js +5 -64
  27. package/src/lib/11ty/paths.js +103 -0
  28. package/src/lib/README.md +0 -330
  29. package/src/lib/conf/README.md +69 -86
  30. package/src/lib/conf/config.js +3 -5
  31. package/src/lib/conf/defaults.js +0 -65
  32. package/src/lib/conf/schema.js +9 -103
  33. package/src/lib/epub/README.md +2 -133
  34. package/src/lib/epub/epub.js +11 -36
  35. package/src/lib/epub/index.js +21 -111
  36. package/src/lib/epub/pandoc.js +4 -33
  37. package/src/lib/git/README.md +2 -151
  38. package/src/lib/git/index.js +13 -217
  39. package/src/lib/i18n/README.md +3 -0
  40. package/src/lib/i18n/config.js +53 -0
  41. package/src/lib/i18n/index.js +43 -0
  42. package/src/lib/i18n/localeService.js +58 -0
  43. package/src/lib/pdf/README.md +0 -131
  44. package/src/lib/pdf/index.js +21 -124
  45. package/src/lib/pdf/paged.js +52 -100
  46. package/src/lib/pdf/prince.js +37 -115
  47. package/src/lib/pdf/split.js +33 -61
  48. package/src/lib/quire/README.md +19 -0
  49. package/src/lib/quire/index.js +353 -0
  50. package/src/lib/reporter/README.md +2 -211
  51. package/src/lib/reporter/index.js +0 -607
  52. package/src/main.js +48 -221
  53. package/src/packageConfig.js +0 -17
  54. package/src/validators/utils.js +1 -2
  55. package/man/quire.1 +0 -341
  56. package/man/quire.1.md +0 -232
  57. package/schemas/config.schema.json +0 -194
  58. package/schemas/figures.schema.json +0 -56
  59. package/schemas/layout.schema.json +0 -5
  60. package/schemas/objects.schema.json +0 -62
  61. package/schemas/publication.schema.json +0 -140
  62. package/schemas/references.schema.json +0 -29
  63. package/src/Command.spec.js +0 -99
  64. package/src/commands/build.spec.js +0 -113
  65. package/src/commands/build.test.js +0 -397
  66. package/src/commands/clean.spec.js +0 -118
  67. package/src/commands/clean.test.js +0 -382
  68. package/src/commands/config.js +0 -238
  69. package/src/commands/config.spec.js +0 -107
  70. package/src/commands/config.test.js +0 -845
  71. package/src/commands/create.spec.js +0 -112
  72. package/src/commands/create.test.js +0 -415
  73. package/src/commands/doctor.js +0 -251
  74. package/src/commands/doctor.spec.js +0 -115
  75. package/src/commands/doctor.test.js +0 -1409
  76. package/src/commands/epub.spec.js +0 -114
  77. package/src/commands/epub.test.js +0 -503
  78. package/src/commands/help.js +0 -60
  79. package/src/commands/help.test.js +0 -132
  80. package/src/commands/info.spec.js +0 -72
  81. package/src/commands/info.test.js +0 -512
  82. package/src/commands/pdf.spec.js +0 -114
  83. package/src/commands/pdf.test.js +0 -464
  84. package/src/commands/preview.spec.js +0 -97
  85. package/src/commands/preview.test.js +0 -250
  86. package/src/commands/use.js +0 -56
  87. package/src/commands/use.spec.js +0 -61
  88. package/src/commands/use.test.js +0 -280
  89. package/src/commands/validate.spec.js +0 -90
  90. package/src/commands/validate.test.js +0 -378
  91. package/src/commands/workflows.js +0 -70
  92. package/src/errors/build/build-failed-error.js +0 -19
  93. package/src/errors/build/config-field-missing-error.js +0 -20
  94. package/src/errors/build/config-file-not-found-error.js +0 -20
  95. package/src/errors/build/index.js +0 -11
  96. package/src/errors/help/help-topic-not-found-error.js +0 -30
  97. package/src/errors/help/index.js +0 -8
  98. package/src/errors/index.js +0 -58
  99. package/src/errors/input/index.js +0 -8
  100. package/src/errors/input/invalid-input-error.js +0 -21
  101. package/src/errors/install/dependency-install-error.js +0 -19
  102. package/src/errors/install/directory-not-empty-error.js +0 -25
  103. package/src/errors/install/index.js +0 -12
  104. package/src/errors/install/invalid-path-error.js +0 -31
  105. package/src/errors/install/invalid-starter-error.js +0 -27
  106. package/src/errors/install/version-not-found-error.js +0 -35
  107. package/src/errors/output/epub-generation-error.js +0 -19
  108. package/src/errors/output/index.js +0 -14
  109. package/src/errors/output/invalid-epub-library-error.js +0 -20
  110. package/src/errors/output/invalid-pdf-library-error.js +0 -20
  111. package/src/errors/output/missing-build-output-error.js +0 -21
  112. package/src/errors/output/pdf-generation-error.js +0 -24
  113. package/src/errors/output/tool-not-found-error.js +0 -37
  114. package/src/errors/project/index.js +0 -10
  115. package/src/errors/project/not-in-project-error.js +0 -20
  116. package/src/errors/project/project-create-error.js +0 -21
  117. package/src/errors/quire-error.js +0 -27
  118. package/src/errors/validation/index.js +0 -3
  119. package/src/errors/validation/unknown-config-key-error.js +0 -27
  120. package/src/errors/validation/unknown-config-operation-error.js +0 -24
  121. package/src/helpers/docs-url.js +0 -32
  122. package/src/helpers/pager.js +0 -59
  123. package/src/helpers/pager.test.js +0 -85
  124. package/src/helpers/suggest-similar.js +0 -124
  125. package/src/helpers/suggest-similar.test.js +0 -108
  126. package/src/helpers/test-cwd.test.js +0 -192
  127. package/src/lib/11ty/index.test.js +0 -655
  128. package/src/lib/commander/index.js +0 -103
  129. package/src/lib/commander/index.test.js +0 -86
  130. package/src/lib/commander/options.js +0 -237
  131. package/src/lib/commander/options.test.js +0 -205
  132. package/src/lib/conf/build-status.js +0 -103
  133. package/src/lib/conf/build-status.test.js +0 -247
  134. package/src/lib/conf/config.test.js +0 -281
  135. package/src/lib/conf/format.js +0 -60
  136. package/src/lib/conf/format.test.js +0 -106
  137. package/src/lib/conf/helpers.js +0 -91
  138. package/src/lib/conf/helpers.test.js +0 -136
  139. package/src/lib/conf/index.js +0 -22
  140. package/src/lib/constants.js +0 -28
  141. package/src/lib/doctor/README.md +0 -667
  142. package/src/lib/doctor/checks/environment/cli-version.js +0 -62
  143. package/src/lib/doctor/checks/environment/cli-version.test.js +0 -132
  144. package/src/lib/doctor/checks/environment/git-available.js +0 -77
  145. package/src/lib/doctor/checks/environment/git-available.test.js +0 -52
  146. package/src/lib/doctor/checks/environment/index.js +0 -13
  147. package/src/lib/doctor/checks/environment/node-version.js +0 -66
  148. package/src/lib/doctor/checks/environment/node-version.test.js +0 -17
  149. package/src/lib/doctor/checks/environment/npm-available.js +0 -66
  150. package/src/lib/doctor/checks/environment/npm-available.test.js +0 -52
  151. package/src/lib/doctor/checks/environment/os-info.js +0 -48
  152. package/src/lib/doctor/checks/environment/os-info.test.js +0 -81
  153. package/src/lib/doctor/checks/environment/runtime-info.js +0 -51
  154. package/src/lib/doctor/checks/environment/runtime-info.test.js +0 -132
  155. package/src/lib/doctor/checks/outputs/epub-output.js +0 -119
  156. package/src/lib/doctor/checks/outputs/epub-output.test.js +0 -277
  157. package/src/lib/doctor/checks/outputs/index.js +0 -10
  158. package/src/lib/doctor/checks/outputs/pdf-output.js +0 -144
  159. package/src/lib/doctor/checks/outputs/pdf-output.test.js +0 -377
  160. package/src/lib/doctor/checks/outputs/stale-build.js +0 -122
  161. package/src/lib/doctor/checks/outputs/stale-build.test.js +0 -282
  162. package/src/lib/doctor/checks/project/data-files.js +0 -56
  163. package/src/lib/doctor/checks/project/data-files.test.js +0 -125
  164. package/src/lib/doctor/checks/project/dependencies.js +0 -53
  165. package/src/lib/doctor/checks/project/dependencies.test.js +0 -71
  166. package/src/lib/doctor/checks/project/index.js +0 -11
  167. package/src/lib/doctor/checks/project/quire-11ty.js +0 -98
  168. package/src/lib/doctor/checks/project/quire-11ty.test.js +0 -170
  169. package/src/lib/doctor/checks/project/quire-project.js +0 -38
  170. package/src/lib/doctor/checks/project/quire-project.test.js +0 -47
  171. package/src/lib/doctor/checks/tools/index.js +0 -10
  172. package/src/lib/doctor/checks/tools/pandoc-available.js +0 -82
  173. package/src/lib/doctor/checks/tools/pandoc-available.test.js +0 -73
  174. package/src/lib/doctor/checks/tools/prince-available.js +0 -81
  175. package/src/lib/doctor/checks/tools/prince-available.test.js +0 -73
  176. package/src/lib/doctor/constants.js +0 -39
  177. package/src/lib/doctor/formatDuration.js +0 -108
  178. package/src/lib/doctor/formatDuration.test.js +0 -76
  179. package/src/lib/doctor/formatters/human.js +0 -257
  180. package/src/lib/doctor/formatters/human.test.js +0 -463
  181. package/src/lib/doctor/formatters/index.js +0 -8
  182. package/src/lib/doctor/formatters/json.js +0 -78
  183. package/src/lib/doctor/formatters/json.test.js +0 -174
  184. package/src/lib/doctor/formatters/shared.js +0 -129
  185. package/src/lib/doctor/formatters/shared.test.js +0 -194
  186. package/src/lib/doctor/index.js +0 -271
  187. package/src/lib/doctor/index.test.js +0 -797
  188. package/src/lib/epub/engines.js +0 -46
  189. package/src/lib/epub/index.test.js +0 -518
  190. package/src/lib/epub/pandoc.test.js +0 -122
  191. package/src/lib/epub/schema.js +0 -21
  192. package/src/lib/error/README.md +0 -170
  193. package/src/lib/error/handler.js +0 -107
  194. package/src/lib/git/index.spec.js +0 -80
  195. package/src/lib/git/index.test.js +0 -453
  196. package/src/lib/help/frontmatter.js +0 -77
  197. package/src/lib/help/frontmatter.test.js +0 -139
  198. package/src/lib/help/index.js +0 -146
  199. package/src/lib/help/index.test.js +0 -188
  200. package/src/lib/help/topics/configuration.md +0 -77
  201. package/src/lib/help/topics/debugging.md +0 -69
  202. package/src/lib/help/topics/epub.md +0 -74
  203. package/src/lib/help/topics/pdf.md +0 -74
  204. package/src/lib/help/topics/publishing.md +0 -80
  205. package/src/lib/help/topics/workflows.md +0 -50
  206. package/src/lib/installer/index.js +0 -309
  207. package/src/lib/installer/index.spec.js +0 -83
  208. package/src/lib/installer/index.test.js +0 -545
  209. package/src/lib/logger/README.md +0 -424
  210. package/src/lib/logger/debug.js +0 -132
  211. package/src/lib/logger/debug.spec.js +0 -130
  212. package/src/lib/logger/debug.test.js +0 -128
  213. package/src/lib/logger/index.js +0 -228
  214. package/src/lib/logger/index.spec.js +0 -131
  215. package/src/lib/logger/index.test.js +0 -578
  216. package/src/lib/npm/README.md +0 -127
  217. package/src/lib/npm/index.js +0 -198
  218. package/src/lib/npm/index.spec.js +0 -60
  219. package/src/lib/npm/index.test.js +0 -355
  220. package/src/lib/pdf/engines.js +0 -46
  221. package/src/lib/pdf/index.test.js +0 -708
  222. package/src/lib/pdf/paged.test.js +0 -366
  223. package/src/lib/pdf/prince.test.js +0 -202
  224. package/src/lib/pdf/schema.js +0 -21
  225. package/src/lib/pdf/split.test.js +0 -445
  226. package/src/lib/platform.js +0 -95
  227. package/src/lib/process/manager.js +0 -110
  228. package/src/lib/process/manager.test.js +0 -55
  229. package/src/lib/project/build.js +0 -162
  230. package/src/lib/project/build.test.js +0 -275
  231. package/src/lib/project/config.js +0 -48
  232. package/src/lib/project/config.test.js +0 -134
  233. package/src/lib/project/detect.js +0 -47
  234. package/src/lib/project/detect.test.js +0 -157
  235. package/src/lib/project/index.js +0 -56
  236. package/src/lib/project/output-paths.js +0 -87
  237. package/src/lib/project/output-paths.test.js +0 -66
  238. package/src/lib/project/paths.js +0 -272
  239. package/src/lib/project/version.js +0 -110
  240. package/src/lib/project/version.test.js +0 -350
  241. package/src/lib/reporter/index.test.js +0 -862
  242. package/src/main.spec.js +0 -61
  243. package/src/main.test.js +0 -347
  244. package/src/validators/validate-data-files.js +0 -154
  245. package/src/validators/validate-data-files.test.js +0 -217
@@ -1,1409 +0,0 @@
1
- import test from 'ava'
2
- import sinon from 'sinon'
3
- import esmock from 'esmock'
4
-
5
- test.beforeEach((t) => {
6
- t.context.sandbox = sinon.createSandbox()
7
-
8
- // Create mock logger (still used by outputJson for file-write messages)
9
- t.context.mockLogger = {
10
- info: t.context.sandbox.stub(),
11
- error: t.context.sandbox.stub(),
12
- warn: t.context.sandbox.stub(),
13
- debug: t.context.sandbox.stub(),
14
- }
15
- })
16
-
17
- test.afterEach.always((t) => {
18
- t.context.sandbox.restore()
19
- // Reset process.exitCode to prevent tests that exercise failed checks
20
- // from causing AVA to exit with a non-zero code
21
- process.exitCode = undefined
22
- })
23
-
24
- /**
25
- * Helper to create a DoctorCommand with mocked dependencies
26
- */
27
- async function createMockedDoctorCommand(sandbox, mockSections) {
28
- return esmock('./doctor.js', {
29
- '#lib/doctor/index.js': {
30
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
31
- checkSections: [],
32
- SECTION_NAMES: ['environment', 'project', 'outputs'],
33
- CHECK_IDS: ['os', 'cli', 'node', 'npm', 'git', 'project', 'deps', '11ty', 'data', 'build', 'pdf', 'epub'],
34
- },
35
- })
36
- }
37
-
38
- /**
39
- * Helper to stub console methods and return the stubs
40
- *
41
- * Nota bene: doctor outputHuman writes directly to console (not the logger)
42
- * to avoid the [quire] prefix on diagnostic output.
43
- */
44
- function stubConsole(sandbox) {
45
- return {
46
- log: sandbox.stub(console, 'log'),
47
- warn: sandbox.stub(console, 'warn'),
48
- error: sandbox.stub(console, 'error'),
49
- }
50
- }
51
-
52
- /**
53
- * Helper to collect all calls from a stub as strings
54
- */
55
- function getCalls(stub) {
56
- return stub.getCalls().map((call) => call.args[0])
57
- }
58
-
59
- /**
60
- * Helper to check if any stub call matches a pattern
61
- */
62
- function calledWithMatch(stub, pattern) {
63
- return getCalls(stub).some((arg) => arg != null && pattern.test(String(arg)))
64
- }
65
-
66
- // ─────────────────────────────────────────────────────────────────────────────
67
- // Human-readable output tests
68
- //
69
- // Nota bene: these tests are serial because they stub global console methods.
70
- // Doctor outputHuman bypasses the logger to avoid the [quire] prefix.
71
- // ─────────────────────────────────────────────────────────────────────────────
72
-
73
- test.serial('doctor command should run all diagnostic checks with sections', async (t) => {
74
- const { sandbox, mockLogger } = t.context
75
- const consoleStubs = stubConsole(sandbox)
76
-
77
- const mockSections = [
78
- {
79
- section: 'Environment',
80
- results: [
81
- { name: 'Node.js version', ok: true, message: 'v22.0.0' },
82
- { name: 'npm available', ok: true, message: null },
83
- { name: 'Git available', ok: true, message: null },
84
- ],
85
- },
86
- {
87
- section: 'Project',
88
- results: [
89
- { name: 'Quire project', ok: true, message: 'Detected via .quire' },
90
- { name: 'Dependencies', ok: true, message: null },
91
- { name: 'Build status', ok: true, message: 'Build output is up to date' },
92
- ],
93
- },
94
- ]
95
-
96
- const DoctorCommand = await esmock('./doctor.js', {
97
- '#lib/doctor/index.js': {
98
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
99
- },
100
- })
101
-
102
- const command = new DoctorCommand()
103
- command.logger = mockLogger
104
-
105
- await command.action([], {}, command)
106
-
107
- t.true(consoleStubs.log.called, 'should write to console')
108
- t.true(
109
- calledWithMatch(consoleStubs.log, /Running diagnostic checks/),
110
- 'should display diagnostic header'
111
- )
112
- t.true(
113
- calledWithMatch(consoleStubs.log, /Environment/),
114
- 'should display Environment section header'
115
- )
116
- t.true(
117
- calledWithMatch(consoleStubs.log, /Project/),
118
- 'should display Project section header'
119
- )
120
- })
121
-
122
- test.serial('doctor command should report failed checks', async (t) => {
123
- const { sandbox, mockLogger } = t.context
124
- const consoleStubs = stubConsole(sandbox)
125
-
126
- const mockSections = [
127
- {
128
- section: 'Environment',
129
- results: [
130
- { name: 'Node.js version', ok: true, message: 'v22.0.0' },
131
- {
132
- name: 'npm available',
133
- ok: false,
134
- message: 'npm not found in PATH',
135
- remediation: 'Install Node.js to get npm',
136
- docsUrl: 'https://quire.getty.edu/docs-v1/install-uninstall/',
137
- },
138
- ],
139
- },
140
- ]
141
-
142
- const DoctorCommand = await createMockedDoctorCommand(sandbox, mockSections)
143
-
144
- const command = new DoctorCommand()
145
- command.logger = mockLogger
146
-
147
- await command.action([], {}, command)
148
-
149
- t.true(consoleStubs.error.called, 'should write errors to stderr')
150
- t.true(
151
- calledWithMatch(consoleStubs.error, /1 check failed/),
152
- 'should report failure count'
153
- )
154
- })
155
-
156
- test.serial('doctor command should display remediation guidance for failed checks', async (t) => {
157
- const { sandbox, mockLogger } = t.context
158
- const consoleStubs = stubConsole(sandbox)
159
-
160
- const mockSections = [
161
- {
162
- section: 'Environment',
163
- results: [
164
- {
165
- name: 'npm available',
166
- ok: false,
167
- message: 'npm not found in PATH',
168
- remediation: 'Install Node.js to get npm',
169
- docsUrl: 'https://quire.getty.edu/docs-v1/install-uninstall/',
170
- },
171
- ],
172
- },
173
- ]
174
-
175
- const DoctorCommand = await createMockedDoctorCommand(sandbox, mockSections)
176
-
177
- const command = new DoctorCommand()
178
- command.logger = mockLogger
179
-
180
- await command.action([], {}, command)
181
-
182
- // Failed checks output goes through console.error as a single joined string
183
- t.true(
184
- calledWithMatch(consoleStubs.error, /How to fix/),
185
- 'should display "How to fix" header'
186
- )
187
- t.true(
188
- calledWithMatch(consoleStubs.error, /Install Node.js/),
189
- 'should display remediation text'
190
- )
191
- t.true(
192
- calledWithMatch(consoleStubs.error, /Documentation:/),
193
- 'should display documentation label'
194
- )
195
- t.true(
196
- calledWithMatch(consoleStubs.error, /quire\.getty\.edu/),
197
- 'should display documentation URL'
198
- )
199
- })
200
-
201
- test.serial('doctor command should display warnings with warning indicator', async (t) => {
202
- const { sandbox, mockLogger } = t.context
203
- const consoleStubs = stubConsole(sandbox)
204
-
205
- const mockSections = [
206
- {
207
- section: 'Project',
208
- results: [
209
- {
210
- name: 'Build status',
211
- ok: false,
212
- level: 'warn',
213
- message: 'Build output is 1 hour older than source files',
214
- remediation: 'Run quire build to regenerate',
215
- },
216
- ],
217
- },
218
- ]
219
-
220
- const DoctorCommand = await esmock('./doctor.js', {
221
- '#lib/doctor/index.js': {
222
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
223
- },
224
- })
225
-
226
- const command = new DoctorCommand()
227
- command.logger = mockLogger
228
-
229
- await command.action([], {}, command)
230
-
231
- // Warnings go through console.warn
232
- t.true(consoleStubs.warn.called, 'should write warnings to stderr')
233
- t.true(
234
- calledWithMatch(consoleStubs.warn, /⚠/),
235
- 'should display warning indicator'
236
- )
237
- t.true(
238
- calledWithMatch(consoleStubs.warn, /All checks passed with 1 warning/),
239
- 'should report passed with warning count'
240
- )
241
- })
242
-
243
- test.serial('doctor command should report all checks passed when healthy', async (t) => {
244
- const { sandbox, mockLogger } = t.context
245
- const consoleStubs = stubConsole(sandbox)
246
-
247
- const mockSections = [
248
- {
249
- section: 'Environment',
250
- results: [
251
- { name: 'Node.js version', ok: true, message: 'v22.0.0' },
252
- { name: 'npm available', ok: true, message: null },
253
- { name: 'Git available', ok: true, message: null },
254
- ],
255
- },
256
- ]
257
-
258
- const DoctorCommand = await esmock('./doctor.js', {
259
- '#lib/doctor/index.js': {
260
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
261
- },
262
- })
263
-
264
- const command = new DoctorCommand()
265
- command.logger = mockLogger
266
-
267
- await command.action([], {}, command)
268
-
269
- t.true(
270
- calledWithMatch(consoleStubs.log, /All checks passed/),
271
- 'should report all checks passed'
272
- )
273
- })
274
-
275
- test.serial('doctor command should display check messages', async (t) => {
276
- const { sandbox, mockLogger } = t.context
277
- const consoleStubs = stubConsole(sandbox)
278
-
279
- const mockSections = [
280
- {
281
- section: 'Environment',
282
- results: [
283
- { name: 'Node.js version', ok: true, message: 'v22.0.0 (>= 22 required)' },
284
- ],
285
- },
286
- ]
287
-
288
- const DoctorCommand = await esmock('./doctor.js', {
289
- '#lib/doctor/index.js': {
290
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
291
- },
292
- })
293
-
294
- const command = new DoctorCommand()
295
- command.logger = mockLogger
296
-
297
- await command.action([], {}, command)
298
-
299
- t.true(
300
- calledWithMatch(consoleStubs.log, /v22\.0\.0/),
301
- 'should display check message'
302
- )
303
- })
304
-
305
- // ─────────────────────────────────────────────────────────────────────────────
306
- // Exit code tests
307
- // ─────────────────────────────────────────────────────────────────────────────
308
-
309
- test.serial('doctor command should set exitCode 1 when checks fail', async (t) => {
310
- const { sandbox, mockLogger } = t.context
311
- stubConsole(sandbox)
312
-
313
- const savedExitCode = process.exitCode
314
-
315
- try {
316
- const mockSections = [
317
- {
318
- section: 'Environment',
319
- results: [
320
- {
321
- name: 'npm available',
322
- ok: false,
323
- message: 'npm not found in PATH',
324
- remediation: 'Install Node.js to get npm',
325
- },
326
- ],
327
- },
328
- ]
329
-
330
- const DoctorCommand = await createMockedDoctorCommand(sandbox, mockSections)
331
-
332
- const command = new DoctorCommand()
333
- command.logger = mockLogger
334
-
335
- await command.action([], {}, command)
336
-
337
- t.is(process.exitCode, 1, 'should set process.exitCode to 1')
338
- } finally {
339
- process.exitCode = savedExitCode
340
- }
341
- })
342
-
343
- test.serial('doctor command should not set exitCode when all checks pass', async (t) => {
344
- const { sandbox, mockLogger } = t.context
345
- stubConsole(sandbox)
346
-
347
- const savedExitCode = process.exitCode
348
- process.exitCode = undefined
349
-
350
- try {
351
- const mockSections = [
352
- {
353
- section: 'Environment',
354
- results: [
355
- { name: 'Node.js version', ok: true, message: 'v22.0.0' },
356
- { name: 'npm available', ok: true, message: null },
357
- ],
358
- },
359
- ]
360
-
361
- const DoctorCommand = await createMockedDoctorCommand(sandbox, mockSections)
362
-
363
- const command = new DoctorCommand()
364
- command.logger = mockLogger
365
-
366
- await command.action([], {}, command)
367
-
368
- t.is(process.exitCode, undefined, 'should not set process.exitCode on success')
369
- } finally {
370
- process.exitCode = savedExitCode
371
- }
372
- })
373
-
374
- test.serial('doctor command should not set exitCode when only warnings exist', async (t) => {
375
- const { sandbox, mockLogger } = t.context
376
- const consoleStubs = stubConsole(sandbox)
377
-
378
- const savedExitCode = process.exitCode
379
- process.exitCode = undefined
380
-
381
- try {
382
- const mockSections = [
383
- {
384
- section: 'Project',
385
- results: [
386
- {
387
- name: 'Build status',
388
- ok: false,
389
- level: 'warn',
390
- message: 'Build output is stale',
391
- remediation: 'Run quire build',
392
- },
393
- ],
394
- },
395
- ]
396
-
397
- const DoctorCommand = await createMockedDoctorCommand(sandbox, mockSections)
398
-
399
- const command = new DoctorCommand()
400
- command.logger = mockLogger
401
-
402
- await command.action([], {}, command)
403
-
404
- t.is(process.exitCode, undefined, 'should not set process.exitCode for warnings')
405
- t.true(
406
- calledWithMatch(consoleStubs.warn, /All checks passed with 1 warning/),
407
- 'should report passed with warning count'
408
- )
409
- } finally {
410
- process.exitCode = savedExitCode
411
- }
412
- })
413
-
414
- // ─────────────────────────────────────────────────────────────────────────────
415
- // Summary count tests
416
- // ─────────────────────────────────────────────────────────────────────────────
417
-
418
- test.serial('doctor command should display plural error count', async (t) => {
419
- const { sandbox, mockLogger } = t.context
420
- const consoleStubs = stubConsole(sandbox)
421
-
422
- const mockSections = [
423
- {
424
- section: 'Environment',
425
- results: [
426
- { name: 'npm', ok: false, message: 'not found' },
427
- { name: 'Git', ok: false, message: 'not found' },
428
- ],
429
- },
430
- ]
431
-
432
- const DoctorCommand = await createMockedDoctorCommand(sandbox, mockSections)
433
-
434
- const command = new DoctorCommand()
435
- command.logger = mockLogger
436
-
437
- await command.action([], {}, command)
438
-
439
- t.true(
440
- calledWithMatch(consoleStubs.error, /2 checks failed/),
441
- 'should use plural for multiple errors'
442
- )
443
- })
444
-
445
- test.serial('doctor command should display errors and warnings together', async (t) => {
446
- const { sandbox, mockLogger } = t.context
447
- const consoleStubs = stubConsole(sandbox)
448
-
449
- const mockSections = [
450
- {
451
- section: 'Environment',
452
- results: [
453
- { name: 'npm', ok: false, message: 'not found' },
454
- { name: 'Git', ok: false, level: 'warn', message: 'outdated' },
455
- { name: 'Prince', ok: false, level: 'warn', message: 'not found' },
456
- ],
457
- },
458
- ]
459
-
460
- const DoctorCommand = await createMockedDoctorCommand(sandbox, mockSections)
461
-
462
- const command = new DoctorCommand()
463
- command.logger = mockLogger
464
-
465
- await command.action([], {}, command)
466
-
467
- t.true(
468
- calledWithMatch(consoleStubs.error, /1 check failed, 2 warnings/),
469
- 'should display both error and warning counts'
470
- )
471
- })
472
-
473
- test.serial('doctor command should display plural warning count', async (t) => {
474
- const { sandbox, mockLogger } = t.context
475
- const consoleStubs = stubConsole(sandbox)
476
-
477
- const mockSections = [
478
- {
479
- section: 'Project',
480
- results: [
481
- { name: 'Build', ok: false, level: 'warn', message: 'stale' },
482
- { name: 'PDF', ok: false, level: 'warn', message: 'stale' },
483
- ],
484
- },
485
- ]
486
-
487
- const DoctorCommand = await esmock('./doctor.js', {
488
- '#lib/doctor/index.js': {
489
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
490
- },
491
- })
492
-
493
- const command = new DoctorCommand()
494
- command.logger = mockLogger
495
-
496
- await command.action([], {}, command)
497
-
498
- t.true(
499
- calledWithMatch(consoleStubs.warn, /All checks passed with 2 warnings/),
500
- 'should use plural for multiple warnings'
501
- )
502
- })
503
-
504
- // ─────────────────────────────────────────────────────────────────────────────
505
- // N/A (not applicable) state tests
506
- // ─────────────────────────────────────────────────────────────────────────────
507
-
508
- test.serial('doctor command should display N/A checks with open circle indicator', async (t) => {
509
- const { sandbox, mockLogger } = t.context
510
- const consoleStubs = stubConsole(sandbox)
511
-
512
- const mockSections = [
513
- {
514
- section: 'Outputs',
515
- results: [
516
- {
517
- name: 'Build status',
518
- ok: true,
519
- level: 'na',
520
- message: 'No build output yet (run quire build)',
521
- },
522
- {
523
- name: 'PDF output',
524
- ok: true,
525
- level: 'na',
526
- message: 'No PDF output (run quire pdf to generate)',
527
- },
528
- ],
529
- },
530
- ]
531
-
532
- const DoctorCommand = await esmock('./doctor.js', {
533
- '#lib/doctor/index.js': {
534
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
535
- },
536
- })
537
-
538
- const command = new DoctorCommand()
539
- command.logger = mockLogger
540
-
541
- await command.action([], {}, command)
542
-
543
- // N/A checks should use ○ indicator
544
- t.true(
545
- calledWithMatch(consoleStubs.log, /○/),
546
- 'should display open circle indicator for N/A checks'
547
- )
548
- })
549
-
550
- test.serial('doctor command should not count N/A checks as failures in summary', async (t) => {
551
- const { sandbox, mockLogger } = t.context
552
- const consoleStubs = stubConsole(sandbox)
553
-
554
- const mockSections = [
555
- {
556
- section: 'Outputs',
557
- results: [
558
- { name: 'Build status', ok: true, level: 'na', message: 'No build output' },
559
- { name: 'PDF output', ok: true, level: 'na', message: 'No PDF output' },
560
- { name: 'EPUB output', ok: true, level: 'na', message: 'No EPUB output' },
561
- ],
562
- },
563
- ]
564
-
565
- const DoctorCommand = await esmock('./doctor.js', {
566
- '#lib/doctor/index.js': {
567
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
568
- },
569
- })
570
-
571
- const command = new DoctorCommand()
572
- command.logger = mockLogger
573
-
574
- await command.action([], {}, command)
575
-
576
- // Should report all checks passed (N/A checks don't count as failures)
577
- t.true(
578
- calledWithMatch(consoleStubs.log, /All checks passed/),
579
- 'should report all checks passed when only N/A checks exist'
580
- )
581
- // Should include N/A count in summary
582
- t.true(
583
- calledWithMatch(consoleStubs.log, /3 not applicable/),
584
- 'should display N/A count in summary'
585
- )
586
- })
587
-
588
- test.serial('doctor command should not count N/A checks as failures when mixed with errors', async (t) => {
589
- const { sandbox, mockLogger } = t.context
590
- const consoleStubs = stubConsole(sandbox)
591
-
592
- const mockSections = [
593
- {
594
- section: 'Environment',
595
- results: [
596
- { name: 'npm', ok: false, message: 'not found' },
597
- ],
598
- },
599
- {
600
- section: 'Outputs',
601
- results: [
602
- { name: 'Build status', ok: true, level: 'na', message: 'No build output' },
603
- { name: 'PDF output', ok: true, level: 'na', message: 'No PDF output' },
604
- ],
605
- },
606
- ]
607
-
608
- const DoctorCommand = await createMockedDoctorCommand(sandbox, mockSections)
609
-
610
- const command = new DoctorCommand()
611
- command.logger = mockLogger
612
-
613
- await command.action([], {}, command)
614
-
615
- // Should only report 1 error (N/A checks not counted as failures)
616
- t.true(
617
- calledWithMatch(consoleStubs.error, /1 check failed/),
618
- 'should only count actual errors, not N/A checks'
619
- )
620
- // Should include N/A count in summary
621
- t.true(
622
- calledWithMatch(consoleStubs.error, /2 not applicable/),
623
- 'should display N/A count alongside error count'
624
- )
625
- })
626
-
627
- test.serial('doctor command should route N/A checks to stdout', async (t) => {
628
- const { sandbox, mockLogger } = t.context
629
- const consoleStubs = stubConsole(sandbox)
630
-
631
- const mockSections = [
632
- {
633
- section: 'Outputs',
634
- results: [
635
- { name: 'PDF output', ok: true, level: 'na', message: 'No PDF output' },
636
- ],
637
- },
638
- ]
639
-
640
- const DoctorCommand = await esmock('./doctor.js', {
641
- '#lib/doctor/index.js': {
642
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
643
- },
644
- })
645
-
646
- const command = new DoctorCommand()
647
- command.logger = mockLogger
648
-
649
- await command.action([], {}, command)
650
-
651
- // N/A should go through console.log (stdout), not console.warn or console.error
652
- const logCalls = getCalls(consoleStubs.log)
653
- const hasNaInLog = logCalls.some((arg) => arg && arg.includes('○') && arg.includes('PDF output'))
654
- t.true(hasNaInLog, 'N/A checks should be routed to stdout via console.log')
655
- })
656
-
657
- // ─────────────────────────────────────────────────────────────────────────────
658
- // Verbose flag tests
659
- // ─────────────────────────────────────────────────────────────────────────────
660
-
661
- test.serial('doctor command should display details when --verbose is passed', async (t) => {
662
- const { sandbox, mockLogger } = t.context
663
- const consoleStubs = stubConsole(sandbox)
664
-
665
- const mockSections = [
666
- {
667
- section: 'Environment',
668
- results: [
669
- { name: 'npm', ok: true, message: '10.2.4', details: '/usr/local/bin/npm' },
670
- { name: 'Git', ok: true, message: '2.43.0', details: '/usr/bin/git' },
671
- ],
672
- },
673
- ]
674
-
675
- const DoctorCommand = await esmock('./doctor.js', {
676
- '#lib/doctor/index.js': {
677
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
678
- },
679
- })
680
-
681
- const command = new DoctorCommand()
682
- command.logger = mockLogger
683
-
684
- await command.action([], { verbose: true }, command)
685
-
686
- // Should display details for each check
687
- t.true(
688
- calledWithMatch(consoleStubs.log, /\/usr\/local\/bin\/npm/),
689
- 'should display npm path in verbose mode'
690
- )
691
- t.true(
692
- calledWithMatch(consoleStubs.log, /\/usr\/bin\/git/),
693
- 'should display git path in verbose mode'
694
- )
695
- })
696
-
697
- test.serial('doctor command should not display details when --verbose is not passed', async (t) => {
698
- const { sandbox, mockLogger } = t.context
699
- const consoleStubs = stubConsole(sandbox)
700
-
701
- const mockSections = [
702
- {
703
- section: 'Environment',
704
- results: [
705
- { name: 'npm', ok: true, message: '10.2.4', details: '/usr/local/bin/npm' },
706
- ],
707
- },
708
- ]
709
-
710
- const DoctorCommand = await esmock('./doctor.js', {
711
- '#lib/doctor/index.js': {
712
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
713
- },
714
- })
715
-
716
- const command = new DoctorCommand()
717
- command.logger = mockLogger
718
-
719
- await command.action([], {}, command)
720
-
721
- // Should not display details path
722
- const allCalls = getCalls(consoleStubs.log)
723
- const hasPathInOutput = allCalls.some((arg) => arg && arg.includes('/usr/local/bin/npm'))
724
- t.false(hasPathInOutput, 'should not display npm path without verbose flag')
725
- })
726
-
727
- test.serial('doctor command should handle checks without details in verbose mode', async (t) => {
728
- const { sandbox, mockLogger } = t.context
729
- const consoleStubs = stubConsole(sandbox)
730
-
731
- const mockSections = [
732
- {
733
- section: 'Environment',
734
- results: [
735
- { name: 'Node.js version', ok: true, message: 'v22.0.0' }, // no details field
736
- ],
737
- },
738
- ]
739
-
740
- const DoctorCommand = await esmock('./doctor.js', {
741
- '#lib/doctor/index.js': {
742
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
743
- },
744
- })
745
-
746
- const command = new DoctorCommand()
747
- command.logger = mockLogger
748
-
749
- // Should not throw when details is undefined
750
- await t.notThrowsAsync(async () => {
751
- await command.action([], { verbose: true }, command)
752
- })
753
-
754
- t.true(calledWithMatch(consoleStubs.log, /v22\.0\.0/), 'should still display message')
755
- })
756
-
757
- test.serial('doctor command should display symbol key in verbose mode', async (t) => {
758
- const { sandbox, mockLogger } = t.context
759
- const consoleStubs = stubConsole(sandbox)
760
-
761
- const mockSections = [
762
- {
763
- section: 'Environment',
764
- results: [
765
- { name: 'Node.js version', ok: true, message: 'v22.0.0' },
766
- ],
767
- },
768
- ]
769
-
770
- const DoctorCommand = await esmock('./doctor.js', {
771
- '#lib/doctor/index.js': {
772
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
773
- },
774
- })
775
-
776
- const command = new DoctorCommand()
777
- command.logger = mockLogger
778
-
779
- await command.action([], { verbose: true }, command)
780
-
781
- t.true(
782
- calledWithMatch(consoleStubs.log, /Key:.*✓.*passed.*✗.*failed.*⚠.*warning.*○.*not applicable.*not yet generated/),
783
- 'should display symbol key'
784
- )
785
- })
786
-
787
- test.serial('doctor command should display symbol key in default mode', async (t) => {
788
- const { sandbox, mockLogger } = t.context
789
- const consoleStubs = stubConsole(sandbox)
790
-
791
- const mockSections = [
792
- {
793
- section: 'Environment',
794
- results: [
795
- { name: 'Node.js version', ok: true, message: 'v22.0.0' },
796
- ],
797
- },
798
- ]
799
-
800
- const DoctorCommand = await esmock('./doctor.js', {
801
- '#lib/doctor/index.js': {
802
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
803
- },
804
- })
805
-
806
- const command = new DoctorCommand()
807
- command.logger = mockLogger
808
-
809
- await command.action([], {}, command)
810
-
811
- const allCalls = getCalls(consoleStubs.log)
812
- const hasKey = allCalls.some((arg) => arg && arg.includes('Key:'))
813
- t.true(hasKey, 'should display symbol key in default mode')
814
- })
815
-
816
- // ─────────────────────────────────────────────────────────────────────────────
817
- // JSON output tests
818
- // ─────────────────────────────────────────────────────────────────────────────
819
-
820
- test.serial('doctor command should output valid JSON when --json flag is used', async (t) => {
821
- const { sandbox, mockLogger } = t.context
822
-
823
- const mockSections = [
824
- {
825
- section: 'Environment',
826
- results: [
827
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
828
- { id: 'npm', name: 'npm', ok: true, message: '10.2.4', details: '/usr/local/bin/npm' },
829
- ],
830
- },
831
- ]
832
-
833
- // Capture console.log output
834
- const consoleLogStub = sandbox.stub(console, 'log')
835
-
836
- const DoctorCommand = await esmock('./doctor.js', {
837
- '#lib/doctor/index.js': {
838
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
839
- },
840
- })
841
-
842
- const command = new DoctorCommand()
843
- command.logger = mockLogger
844
-
845
- await command.action([], { json: true }, command)
846
-
847
- // Should call console.log with JSON
848
- t.true(consoleLogStub.calledOnce, 'should call console.log once')
849
-
850
- const output = consoleLogStub.firstCall.args[0]
851
- const parsed = JSON.parse(output)
852
-
853
- t.truthy(parsed.summary, 'should have summary object')
854
- t.is(parsed.summary.passed, 2, 'should count passed checks')
855
- t.is(parsed.summary.failed, 0, 'should count failed checks')
856
- t.truthy(parsed.results, 'should have results array')
857
- t.is(parsed.results.length, 2, 'should include all results')
858
- t.is(parsed.results[0].id, 'node', 'check should have id')
859
- t.is(parsed.results[0].status, 'passed', 'check should have status')
860
- })
861
-
862
- test.serial('doctor command JSON output should set exitCode 1 when checks fail', async (t) => {
863
- const { sandbox, mockLogger } = t.context
864
-
865
- const consoleLogStub = sandbox.stub(console, 'log')
866
- const savedExitCode = process.exitCode
867
-
868
- try {
869
- const mockSections = [
870
- {
871
- section: 'Environment',
872
- results: [
873
- { id: 'node', name: 'Node.js version', ok: false, message: 'v18.0.0 found' },
874
- ],
875
- },
876
- ]
877
-
878
- const DoctorCommand = await esmock('./doctor.js', {
879
- '#lib/doctor/index.js': {
880
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
881
- },
882
- })
883
-
884
- const command = new DoctorCommand()
885
- command.logger = mockLogger
886
-
887
- await command.action([], { json: true }, command)
888
-
889
- t.is(process.exitCode, 1, 'should set process.exitCode to 1')
890
-
891
- const output = consoleLogStub.firstCall.args[0]
892
- const parsed = JSON.parse(output)
893
- t.is(parsed.summary.failed, 1, 'should count failed check')
894
- } finally {
895
- process.exitCode = savedExitCode
896
- }
897
- })
898
-
899
- test.serial('doctor command JSON output should include remediation for failed checks', async (t) => {
900
- const { sandbox, mockLogger } = t.context
901
-
902
- const consoleLogStub = sandbox.stub(console, 'log')
903
-
904
- const mockSections = [
905
- {
906
- section: 'Environment',
907
- results: [
908
- {
909
- id: 'node',
910
- name: 'Node.js version',
911
- ok: false,
912
- message: 'v18.0.0 found',
913
- remediation: 'Install Node.js 22+',
914
- docsUrl: 'https://example.com/docs',
915
- },
916
- ],
917
- },
918
- ]
919
-
920
- const DoctorCommand = await esmock('./doctor.js', {
921
- '#lib/doctor/index.js': {
922
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
923
- },
924
- })
925
-
926
- const command = new DoctorCommand()
927
- command.logger = mockLogger
928
-
929
- await command.action([], { json: true }, command)
930
-
931
- const output = consoleLogStub.firstCall.args[0]
932
- const parsed = JSON.parse(output)
933
-
934
- t.is(parsed.results[0].remediation, 'Install Node.js 22+', 'should include remediation')
935
- t.is(parsed.results[0].docsUrl, 'https://example.com/docs', 'should include docsUrl')
936
- })
937
-
938
- // ─────────────────────────────────────────────────────────────────────────────
939
- // --errors and --warnings filter tests
940
- // ─────────────────────────────────────────────────────────────────────────────
941
-
942
- test.serial('doctor command --errors flag should show only failed checks', async (t) => {
943
- const { sandbox, mockLogger } = t.context
944
- const consoleStubs = stubConsole(sandbox)
945
-
946
- const mockSections = [
947
- {
948
- section: 'Environment',
949
- results: [
950
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
951
- { id: 'npm', name: 'npm', ok: false, message: 'not found' },
952
- { id: 'git', name: 'Git', ok: false, level: 'warn', message: 'old version' },
953
- ],
954
- },
955
- ]
956
-
957
- const DoctorCommand = await esmock('./doctor.js', {
958
- '#lib/doctor/index.js': {
959
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
960
- },
961
- })
962
-
963
- const command = new DoctorCommand()
964
- command.logger = mockLogger
965
-
966
- await command.action([], { errors: true }, command)
967
-
968
- // Should only show failed checks (not passed or warnings)
969
- const errorCalls = getCalls(consoleStubs.error)
970
- const hasNpm = errorCalls.some((arg) => arg && arg.includes('npm'))
971
- t.true(hasNpm, 'should show failed npm check')
972
-
973
- // Should not show passed checks in output
974
- const logCalls = getCalls(consoleStubs.log)
975
- const hasNode = logCalls.some((arg) => arg && arg.includes('Node.js') && arg.includes('✓'))
976
- t.false(hasNode, 'should not show passed Node.js check')
977
- })
978
-
979
- test.serial('doctor command --warnings flag should show only warning checks', async (t) => {
980
- const { sandbox, mockLogger } = t.context
981
- const consoleStubs = stubConsole(sandbox)
982
-
983
- const mockSections = [
984
- {
985
- section: 'Environment',
986
- results: [
987
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
988
- { id: 'cli', name: 'CLI version', ok: false, level: 'warn', message: 'update available' },
989
- { id: 'npm', name: 'npm', ok: false, message: 'not found' },
990
- ],
991
- },
992
- ]
993
-
994
- const DoctorCommand = await esmock('./doctor.js', {
995
- '#lib/doctor/index.js': {
996
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
997
- },
998
- })
999
-
1000
- const command = new DoctorCommand()
1001
- command.logger = mockLogger
1002
-
1003
- await command.action([], { warnings: true }, command)
1004
-
1005
- // Should show warning checks
1006
- const warnCalls = getCalls(consoleStubs.warn)
1007
- const hasCli = warnCalls.some((arg) => arg && arg.includes('CLI version'))
1008
- t.true(hasCli, 'should show CLI version warning')
1009
-
1010
- // Should not show passed checks
1011
- const logCalls = getCalls(consoleStubs.log)
1012
- const hasNode = logCalls.some((arg) => arg && arg.includes('Node.js') && arg.includes('✓'))
1013
- t.false(hasNode, 'should not show passed Node.js check')
1014
- })
1015
-
1016
- test.serial('doctor command should show "No failed checks found" when --errors finds none', async (t) => {
1017
- const { sandbox, mockLogger } = t.context
1018
- const consoleStubs = stubConsole(sandbox)
1019
-
1020
- const mockSections = [
1021
- {
1022
- section: 'Environment',
1023
- results: [
1024
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
1025
- { id: 'npm', name: 'npm', ok: true, message: '10.2.4' },
1026
- ],
1027
- },
1028
- ]
1029
-
1030
- const DoctorCommand = await esmock('./doctor.js', {
1031
- '#lib/doctor/index.js': {
1032
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1033
- },
1034
- })
1035
-
1036
- const command = new DoctorCommand()
1037
- command.logger = mockLogger
1038
-
1039
- await command.action([], { errors: true }, command)
1040
-
1041
- t.true(
1042
- calledWithMatch(consoleStubs.log, /No failed checks found/),
1043
- 'should show no failed checks message'
1044
- )
1045
- })
1046
-
1047
- test.serial('doctor command should show "No warnings found" when --warnings finds none', async (t) => {
1048
- const { sandbox, mockLogger } = t.context
1049
- const consoleStubs = stubConsole(sandbox)
1050
-
1051
- const mockSections = [
1052
- {
1053
- section: 'Environment',
1054
- results: [
1055
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
1056
- ],
1057
- },
1058
- ]
1059
-
1060
- const DoctorCommand = await esmock('./doctor.js', {
1061
- '#lib/doctor/index.js': {
1062
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1063
- },
1064
- })
1065
-
1066
- const command = new DoctorCommand()
1067
- command.logger = mockLogger
1068
-
1069
- await command.action([], { warnings: true }, command)
1070
-
1071
- t.true(
1072
- calledWithMatch(consoleStubs.log, /No warnings found/),
1073
- 'should show no warnings message'
1074
- )
1075
- })
1076
-
1077
- // ─────────────────────────────────────────────────────────────────────────────
1078
- // --json [file] file output tests
1079
- // ─────────────────────────────────────────────────────────────────────────────
1080
-
1081
- test.serial('doctor command --json <file> should write JSON to file', async (t) => {
1082
- const { sandbox, mockLogger } = t.context
1083
-
1084
- const mockSections = [
1085
- {
1086
- section: 'Environment',
1087
- results: [
1088
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
1089
- ],
1090
- },
1091
- ]
1092
-
1093
- // Mock fs.writeFileSync
1094
- const writeFileSyncStub = sandbox.stub()
1095
-
1096
- const DoctorCommand = await esmock('./doctor.js', {
1097
- 'node:fs': {
1098
- writeFileSync: writeFileSyncStub,
1099
- },
1100
- '#lib/doctor/index.js': {
1101
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1102
- },
1103
- })
1104
-
1105
- const command = new DoctorCommand()
1106
- command.logger = mockLogger
1107
-
1108
- // When --json has a value, options.json is the filename string
1109
- await command.action([], { json: 'report.json' }, command)
1110
-
1111
- t.true(writeFileSyncStub.calledOnce, 'should call writeFileSync once')
1112
- t.true(
1113
- writeFileSyncStub.firstCall.args[0].endsWith('report.json'),
1114
- 'should write to specified file'
1115
- )
1116
-
1117
- // Verify the content is valid JSON
1118
- const writtenContent = writeFileSyncStub.firstCall.args[1]
1119
- t.notThrows(() => JSON.parse(writtenContent.trim()), 'should write valid JSON')
1120
-
1121
- t.true(
1122
- mockLogger.info.calledWith(sinon.match(/Results written to/)),
1123
- 'should log success message'
1124
- )
1125
- })
1126
-
1127
- test.serial('doctor command --json (no file) should write to stdout', async (t) => {
1128
- const { sandbox, mockLogger } = t.context
1129
-
1130
- const mockSections = [
1131
- {
1132
- section: 'Environment',
1133
- results: [
1134
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
1135
- ],
1136
- },
1137
- ]
1138
-
1139
- const consoleLogStub = sandbox.stub(console, 'log')
1140
-
1141
- const DoctorCommand = await esmock('./doctor.js', {
1142
- '#lib/doctor/index.js': {
1143
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1144
- },
1145
- })
1146
-
1147
- const command = new DoctorCommand()
1148
- command.logger = mockLogger
1149
-
1150
- // When --json is used without a value, options.json is true
1151
- await command.action([], { json: true }, command)
1152
-
1153
- t.true(consoleLogStub.calledOnce, 'should write to stdout')
1154
- t.notThrows(() => JSON.parse(consoleLogStub.firstCall.args[0]), 'should output valid JSON')
1155
- })
1156
-
1157
- test.serial('doctor command --json <file> should not write to stdout', async (t) => {
1158
- const { sandbox, mockLogger } = t.context
1159
-
1160
- const mockSections = [
1161
- {
1162
- section: 'Environment',
1163
- results: [
1164
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
1165
- ],
1166
- },
1167
- ]
1168
-
1169
- const consoleLogStub = sandbox.stub(console, 'log')
1170
- const writeFileSyncStub = sandbox.stub()
1171
-
1172
- const DoctorCommand = await esmock('./doctor.js', {
1173
- 'node:fs': {
1174
- writeFileSync: writeFileSyncStub,
1175
- },
1176
- '#lib/doctor/index.js': {
1177
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1178
- },
1179
- })
1180
-
1181
- const command = new DoctorCommand()
1182
- command.logger = mockLogger
1183
-
1184
- await command.action([], { json: 'report.json' }, command)
1185
-
1186
- t.false(consoleLogStub.called, 'should not write to stdout when file is specified')
1187
- t.true(writeFileSyncStub.calledOnce, 'should write to file instead')
1188
- })
1189
-
1190
- // ─────────────────────────────────────────────────────────────────────────────
1191
- // --quiet flag tests
1192
- // ─────────────────────────────────────────────────────────────────────────────
1193
-
1194
- test.serial('doctor command --quiet alone should produce no output on success', async (t) => {
1195
- const { sandbox, mockLogger } = t.context
1196
-
1197
- const mockSections = [
1198
- {
1199
- section: 'Environment',
1200
- results: [
1201
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
1202
- ],
1203
- },
1204
- ]
1205
-
1206
- const consoleLogStub = sandbox.stub(console, 'log')
1207
-
1208
- const DoctorCommand = await esmock('./doctor.js', {
1209
- '#lib/doctor/index.js': {
1210
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1211
- },
1212
- })
1213
-
1214
- const command = new DoctorCommand()
1215
- command.logger = mockLogger
1216
-
1217
- // --quiet alone (no --json) should produce no output
1218
- await command.action([], { quiet: true }, command)
1219
-
1220
- t.false(consoleLogStub.called, 'should not write to stdout')
1221
- t.false(mockLogger.info.called, 'should not log any info messages')
1222
- })
1223
-
1224
- test.serial('doctor command --quiet alone should set exitCode 1 on failures', async (t) => {
1225
- const { sandbox, mockLogger } = t.context
1226
-
1227
- const consoleLogStub = sandbox.stub(console, 'log')
1228
- const savedExitCode = process.exitCode
1229
-
1230
- try {
1231
- const mockSections = [
1232
- {
1233
- section: 'Environment',
1234
- results: [
1235
- { id: 'npm', name: 'npm', ok: false, message: 'not found' },
1236
- ],
1237
- },
1238
- ]
1239
-
1240
- const DoctorCommand = await esmock('./doctor.js', {
1241
- '#lib/doctor/index.js': {
1242
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1243
- },
1244
- })
1245
-
1246
- const command = new DoctorCommand()
1247
- command.logger = mockLogger
1248
-
1249
- // --quiet alone should still set exitCode 1 on failure
1250
- await command.action([], { quiet: true }, command)
1251
-
1252
- t.is(process.exitCode, 1, 'should set process.exitCode to 1')
1253
- t.false(consoleLogStub.called, 'should not output anything')
1254
- t.false(mockLogger.info.called, 'should not log any info messages')
1255
- } finally {
1256
- process.exitCode = savedExitCode
1257
- }
1258
- })
1259
-
1260
- test.serial('doctor command --quiet --json should suppress stdout JSON output', async (t) => {
1261
- const { sandbox, mockLogger } = t.context
1262
-
1263
- const mockSections = [
1264
- {
1265
- section: 'Environment',
1266
- results: [
1267
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
1268
- ],
1269
- },
1270
- ]
1271
-
1272
- const consoleLogStub = sandbox.stub(console, 'log')
1273
-
1274
- const DoctorCommand = await esmock('./doctor.js', {
1275
- '#lib/doctor/index.js': {
1276
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1277
- },
1278
- })
1279
-
1280
- const command = new DoctorCommand()
1281
- command.logger = mockLogger
1282
-
1283
- // --quiet with --json should suppress JSON stdout output
1284
- await command.action([], { quiet: true, json: true }, command)
1285
-
1286
- t.false(consoleLogStub.called, 'should not write JSON to stdout in quiet mode')
1287
- })
1288
-
1289
- test.serial('doctor command --quiet --json <file> should write to file silently', async (t) => {
1290
- const { sandbox, mockLogger } = t.context
1291
-
1292
- const mockSections = [
1293
- {
1294
- section: 'Environment',
1295
- results: [
1296
- { id: 'node', name: 'Node.js version', ok: true, message: 'v22.0.0' },
1297
- ],
1298
- },
1299
- ]
1300
-
1301
- const writeFileSyncStub = sandbox.stub()
1302
-
1303
- const DoctorCommand = await esmock('./doctor.js', {
1304
- 'node:fs': {
1305
- writeFileSync: writeFileSyncStub,
1306
- },
1307
- '#lib/doctor/index.js': {
1308
- runAllChecksWithSections: sandbox.stub().resolves(mockSections),
1309
- },
1310
- })
1311
-
1312
- const command = new DoctorCommand()
1313
- command.logger = mockLogger
1314
-
1315
- await command.action([], { quiet: true, json: 'report.json' }, command)
1316
-
1317
- t.true(writeFileSyncStub.calledOnce, 'should write to file')
1318
- t.false(
1319
- mockLogger.info.calledWith(sinon.match(/Results written to/)),
1320
- 'should not log success message in quiet mode'
1321
- )
1322
- })
1323
-
1324
- // ─────────────────────────────────────────────────────────────────────────────
1325
- // --reset flag tests
1326
- // ─────────────────────────────────────────────────────────────────────────────
1327
-
1328
- test.serial('doctor command --reset should clear build status', async (t) => {
1329
- const { sandbox, mockLogger } = t.context
1330
-
1331
- const clearStatusStub = sandbox.stub()
1332
- const runAllChecksStub = sandbox.stub().resolves([])
1333
-
1334
- const DoctorCommand = await esmock('./doctor.js', {
1335
- '#lib/conf/build-status.js': {
1336
- clearStatus: clearStatusStub,
1337
- },
1338
- '#lib/doctor/index.js': {
1339
- runAllChecksWithSections: runAllChecksStub,
1340
- checkSections: [],
1341
- SECTION_NAMES: ['environment', 'project', 'outputs'],
1342
- CHECK_IDS: ['os', 'cli', 'node', 'npm', 'git', 'project', 'deps', '11ty', 'data', 'build', 'pdf', 'epub'],
1343
- },
1344
- })
1345
-
1346
- const command = new DoctorCommand()
1347
- command.logger = mockLogger
1348
-
1349
- await command.action([], { reset: true }, command)
1350
-
1351
- t.true(clearStatusStub.calledOnce, 'should call clearStatus')
1352
- t.is(clearStatusStub.firstCall.args[0], process.cwd(), 'should clear status for current directory')
1353
- t.true(
1354
- mockLogger.info.calledWith(sinon.match(/Build status cleared/)),
1355
- 'should log confirmation message'
1356
- )
1357
- })
1358
-
1359
- test.serial('doctor command --reset should not run checks', async (t) => {
1360
- const { sandbox, mockLogger } = t.context
1361
-
1362
- const clearStatusStub = sandbox.stub()
1363
- const runAllChecksStub = sandbox.stub().resolves([])
1364
-
1365
- const DoctorCommand = await esmock('./doctor.js', {
1366
- '#lib/conf/build-status.js': {
1367
- clearStatus: clearStatusStub,
1368
- },
1369
- '#lib/doctor/index.js': {
1370
- runAllChecksWithSections: runAllChecksStub,
1371
- checkSections: [],
1372
- SECTION_NAMES: ['environment', 'project', 'outputs'],
1373
- CHECK_IDS: ['os', 'cli', 'node', 'npm', 'git', 'project', 'deps', '11ty', 'data', 'build', 'pdf', 'epub'],
1374
- },
1375
- })
1376
-
1377
- const command = new DoctorCommand()
1378
- command.logger = mockLogger
1379
-
1380
- await command.action([], { reset: true }, command)
1381
-
1382
- t.false(runAllChecksStub.called, 'should not run diagnostic checks')
1383
- })
1384
-
1385
- test.serial('doctor command --reset --quiet should suppress output', async (t) => {
1386
- const { sandbox, mockLogger } = t.context
1387
-
1388
- const clearStatusStub = sandbox.stub()
1389
-
1390
- const DoctorCommand = await esmock('./doctor.js', {
1391
- '#lib/conf/build-status.js': {
1392
- clearStatus: clearStatusStub,
1393
- },
1394
- '#lib/doctor/index.js': {
1395
- runAllChecksWithSections: sandbox.stub().resolves([]),
1396
- checkSections: [],
1397
- SECTION_NAMES: ['environment', 'project', 'outputs'],
1398
- CHECK_IDS: ['os', 'cli', 'node', 'npm', 'git', 'project', 'deps', '11ty', 'data', 'build', 'pdf', 'epub'],
1399
- },
1400
- })
1401
-
1402
- const command = new DoctorCommand()
1403
- command.logger = mockLogger
1404
-
1405
- await command.action([], { reset: true, quiet: true }, command)
1406
-
1407
- t.true(clearStatusStub.calledOnce, 'should still clear status')
1408
- t.false(mockLogger.info.called, 'should not log any messages in quiet mode')
1409
- })