@thegetty/quire-cli 1.0.0-rc.44 → 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 (243) 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 +9 -25
  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/schemas/config.schema.json +0 -194
  56. package/schemas/figures.schema.json +0 -56
  57. package/schemas/layout.schema.json +0 -5
  58. package/schemas/objects.schema.json +0 -62
  59. package/schemas/publication.schema.json +0 -140
  60. package/schemas/references.schema.json +0 -29
  61. package/src/Command.spec.js +0 -99
  62. package/src/commands/build.spec.js +0 -113
  63. package/src/commands/build.test.js +0 -397
  64. package/src/commands/clean.spec.js +0 -118
  65. package/src/commands/clean.test.js +0 -382
  66. package/src/commands/config.js +0 -238
  67. package/src/commands/config.spec.js +0 -107
  68. package/src/commands/config.test.js +0 -845
  69. package/src/commands/create.spec.js +0 -112
  70. package/src/commands/create.test.js +0 -415
  71. package/src/commands/doctor.js +0 -251
  72. package/src/commands/doctor.spec.js +0 -115
  73. package/src/commands/doctor.test.js +0 -1409
  74. package/src/commands/epub.spec.js +0 -114
  75. package/src/commands/epub.test.js +0 -503
  76. package/src/commands/help.js +0 -60
  77. package/src/commands/help.test.js +0 -132
  78. package/src/commands/info.spec.js +0 -72
  79. package/src/commands/info.test.js +0 -512
  80. package/src/commands/pdf.spec.js +0 -114
  81. package/src/commands/pdf.test.js +0 -464
  82. package/src/commands/preview.spec.js +0 -97
  83. package/src/commands/preview.test.js +0 -250
  84. package/src/commands/use.js +0 -56
  85. package/src/commands/use.spec.js +0 -61
  86. package/src/commands/use.test.js +0 -280
  87. package/src/commands/validate.spec.js +0 -90
  88. package/src/commands/validate.test.js +0 -378
  89. package/src/commands/workflows.js +0 -70
  90. package/src/errors/build/build-failed-error.js +0 -19
  91. package/src/errors/build/config-field-missing-error.js +0 -20
  92. package/src/errors/build/config-file-not-found-error.js +0 -20
  93. package/src/errors/build/index.js +0 -11
  94. package/src/errors/help/help-topic-not-found-error.js +0 -30
  95. package/src/errors/help/index.js +0 -8
  96. package/src/errors/index.js +0 -58
  97. package/src/errors/input/index.js +0 -8
  98. package/src/errors/input/invalid-input-error.js +0 -21
  99. package/src/errors/install/dependency-install-error.js +0 -19
  100. package/src/errors/install/directory-not-empty-error.js +0 -25
  101. package/src/errors/install/index.js +0 -12
  102. package/src/errors/install/invalid-path-error.js +0 -31
  103. package/src/errors/install/invalid-starter-error.js +0 -27
  104. package/src/errors/install/version-not-found-error.js +0 -35
  105. package/src/errors/output/epub-generation-error.js +0 -19
  106. package/src/errors/output/index.js +0 -14
  107. package/src/errors/output/invalid-epub-library-error.js +0 -20
  108. package/src/errors/output/invalid-pdf-library-error.js +0 -20
  109. package/src/errors/output/missing-build-output-error.js +0 -21
  110. package/src/errors/output/pdf-generation-error.js +0 -24
  111. package/src/errors/output/tool-not-found-error.js +0 -37
  112. package/src/errors/project/index.js +0 -10
  113. package/src/errors/project/not-in-project-error.js +0 -20
  114. package/src/errors/project/project-create-error.js +0 -21
  115. package/src/errors/quire-error.js +0 -27
  116. package/src/errors/validation/index.js +0 -3
  117. package/src/errors/validation/unknown-config-key-error.js +0 -27
  118. package/src/errors/validation/unknown-config-operation-error.js +0 -24
  119. package/src/helpers/docs-url.js +0 -32
  120. package/src/helpers/pager.js +0 -59
  121. package/src/helpers/pager.test.js +0 -85
  122. package/src/helpers/suggest-similar.js +0 -124
  123. package/src/helpers/suggest-similar.test.js +0 -108
  124. package/src/helpers/test-cwd.test.js +0 -192
  125. package/src/lib/11ty/index.test.js +0 -655
  126. package/src/lib/commander/index.js +0 -103
  127. package/src/lib/commander/index.test.js +0 -86
  128. package/src/lib/commander/options.js +0 -237
  129. package/src/lib/commander/options.test.js +0 -205
  130. package/src/lib/conf/build-status.js +0 -103
  131. package/src/lib/conf/build-status.test.js +0 -247
  132. package/src/lib/conf/config.test.js +0 -281
  133. package/src/lib/conf/format.js +0 -60
  134. package/src/lib/conf/format.test.js +0 -106
  135. package/src/lib/conf/helpers.js +0 -91
  136. package/src/lib/conf/helpers.test.js +0 -136
  137. package/src/lib/conf/index.js +0 -22
  138. package/src/lib/constants.js +0 -28
  139. package/src/lib/doctor/README.md +0 -667
  140. package/src/lib/doctor/checks/environment/cli-version.js +0 -62
  141. package/src/lib/doctor/checks/environment/cli-version.test.js +0 -132
  142. package/src/lib/doctor/checks/environment/git-available.js +0 -77
  143. package/src/lib/doctor/checks/environment/git-available.test.js +0 -52
  144. package/src/lib/doctor/checks/environment/index.js +0 -13
  145. package/src/lib/doctor/checks/environment/node-version.js +0 -66
  146. package/src/lib/doctor/checks/environment/node-version.test.js +0 -17
  147. package/src/lib/doctor/checks/environment/npm-available.js +0 -66
  148. package/src/lib/doctor/checks/environment/npm-available.test.js +0 -52
  149. package/src/lib/doctor/checks/environment/os-info.js +0 -48
  150. package/src/lib/doctor/checks/environment/os-info.test.js +0 -81
  151. package/src/lib/doctor/checks/environment/runtime-info.js +0 -51
  152. package/src/lib/doctor/checks/environment/runtime-info.test.js +0 -132
  153. package/src/lib/doctor/checks/outputs/epub-output.js +0 -119
  154. package/src/lib/doctor/checks/outputs/epub-output.test.js +0 -277
  155. package/src/lib/doctor/checks/outputs/index.js +0 -10
  156. package/src/lib/doctor/checks/outputs/pdf-output.js +0 -144
  157. package/src/lib/doctor/checks/outputs/pdf-output.test.js +0 -377
  158. package/src/lib/doctor/checks/outputs/stale-build.js +0 -122
  159. package/src/lib/doctor/checks/outputs/stale-build.test.js +0 -282
  160. package/src/lib/doctor/checks/project/data-files.js +0 -56
  161. package/src/lib/doctor/checks/project/data-files.test.js +0 -125
  162. package/src/lib/doctor/checks/project/dependencies.js +0 -53
  163. package/src/lib/doctor/checks/project/dependencies.test.js +0 -71
  164. package/src/lib/doctor/checks/project/index.js +0 -11
  165. package/src/lib/doctor/checks/project/quire-11ty.js +0 -98
  166. package/src/lib/doctor/checks/project/quire-11ty.test.js +0 -170
  167. package/src/lib/doctor/checks/project/quire-project.js +0 -38
  168. package/src/lib/doctor/checks/project/quire-project.test.js +0 -47
  169. package/src/lib/doctor/checks/tools/index.js +0 -10
  170. package/src/lib/doctor/checks/tools/pandoc-available.js +0 -82
  171. package/src/lib/doctor/checks/tools/pandoc-available.test.js +0 -73
  172. package/src/lib/doctor/checks/tools/prince-available.js +0 -81
  173. package/src/lib/doctor/checks/tools/prince-available.test.js +0 -73
  174. package/src/lib/doctor/constants.js +0 -39
  175. package/src/lib/doctor/formatDuration.js +0 -108
  176. package/src/lib/doctor/formatDuration.test.js +0 -76
  177. package/src/lib/doctor/formatters/human.js +0 -257
  178. package/src/lib/doctor/formatters/human.test.js +0 -463
  179. package/src/lib/doctor/formatters/index.js +0 -8
  180. package/src/lib/doctor/formatters/json.js +0 -78
  181. package/src/lib/doctor/formatters/json.test.js +0 -174
  182. package/src/lib/doctor/formatters/shared.js +0 -129
  183. package/src/lib/doctor/formatters/shared.test.js +0 -194
  184. package/src/lib/doctor/index.js +0 -271
  185. package/src/lib/doctor/index.test.js +0 -797
  186. package/src/lib/epub/engines.js +0 -46
  187. package/src/lib/epub/index.test.js +0 -518
  188. package/src/lib/epub/pandoc.test.js +0 -122
  189. package/src/lib/epub/schema.js +0 -21
  190. package/src/lib/error/README.md +0 -170
  191. package/src/lib/error/handler.js +0 -107
  192. package/src/lib/git/index.spec.js +0 -80
  193. package/src/lib/git/index.test.js +0 -453
  194. package/src/lib/help/frontmatter.js +0 -77
  195. package/src/lib/help/frontmatter.test.js +0 -139
  196. package/src/lib/help/index.js +0 -146
  197. package/src/lib/help/index.test.js +0 -188
  198. package/src/lib/help/topics/configuration.md +0 -77
  199. package/src/lib/help/topics/debugging.md +0 -69
  200. package/src/lib/help/topics/epub.md +0 -74
  201. package/src/lib/help/topics/pdf.md +0 -74
  202. package/src/lib/help/topics/publishing.md +0 -80
  203. package/src/lib/help/topics/workflows.md +0 -50
  204. package/src/lib/installer/index.js +0 -309
  205. package/src/lib/installer/index.spec.js +0 -83
  206. package/src/lib/installer/index.test.js +0 -545
  207. package/src/lib/logger/README.md +0 -424
  208. package/src/lib/logger/debug.js +0 -132
  209. package/src/lib/logger/debug.spec.js +0 -130
  210. package/src/lib/logger/debug.test.js +0 -128
  211. package/src/lib/logger/index.js +0 -228
  212. package/src/lib/logger/index.spec.js +0 -131
  213. package/src/lib/logger/index.test.js +0 -578
  214. package/src/lib/npm/README.md +0 -127
  215. package/src/lib/npm/index.js +0 -198
  216. package/src/lib/npm/index.spec.js +0 -60
  217. package/src/lib/npm/index.test.js +0 -355
  218. package/src/lib/pdf/engines.js +0 -46
  219. package/src/lib/pdf/index.test.js +0 -708
  220. package/src/lib/pdf/paged.test.js +0 -366
  221. package/src/lib/pdf/prince.test.js +0 -202
  222. package/src/lib/pdf/schema.js +0 -21
  223. package/src/lib/pdf/split.test.js +0 -445
  224. package/src/lib/platform.js +0 -95
  225. package/src/lib/process/manager.js +0 -110
  226. package/src/lib/process/manager.test.js +0 -55
  227. package/src/lib/project/build.js +0 -162
  228. package/src/lib/project/build.test.js +0 -275
  229. package/src/lib/project/config.js +0 -48
  230. package/src/lib/project/config.test.js +0 -134
  231. package/src/lib/project/detect.js +0 -47
  232. package/src/lib/project/detect.test.js +0 -157
  233. package/src/lib/project/index.js +0 -56
  234. package/src/lib/project/output-paths.js +0 -87
  235. package/src/lib/project/output-paths.test.js +0 -66
  236. package/src/lib/project/paths.js +0 -272
  237. package/src/lib/project/version.js +0 -110
  238. package/src/lib/project/version.test.js +0 -350
  239. package/src/lib/reporter/index.test.js +0 -862
  240. package/src/main.spec.js +0 -61
  241. package/src/main.test.js +0 -347
  242. package/src/validators/validate-data-files.js +0 -154
  243. 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
- })