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

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 (104) hide show
  1. package/package.json +5 -2
  2. package/src/commands/build.js +13 -6
  3. package/src/commands/clean.js +10 -2
  4. package/src/commands/clean.spec.js +10 -0
  5. package/src/commands/clean.test.js +122 -0
  6. package/src/commands/config.js +3 -1
  7. package/src/commands/config.test.js +42 -1
  8. package/src/commands/doctor.js +251 -0
  9. package/src/commands/doctor.spec.js +115 -0
  10. package/src/commands/doctor.test.js +1409 -0
  11. package/src/commands/epub.js +9 -2
  12. package/src/commands/help.js +60 -0
  13. package/src/commands/help.test.js +132 -0
  14. package/src/commands/info.js +79 -71
  15. package/src/commands/info.spec.js +8 -0
  16. package/src/commands/info.test.js +173 -76
  17. package/src/commands/pdf.js +10 -3
  18. package/src/commands/validate.js +39 -7
  19. package/src/commands/validate.spec.js +8 -0
  20. package/src/commands/validate.test.js +144 -0
  21. package/src/errors/help/help-topic-not-found-error.js +22 -0
  22. package/src/errors/help/index.js +8 -0
  23. package/src/errors/index.js +6 -0
  24. package/src/errors/input/index.js +8 -0
  25. package/src/errors/input/invalid-input-error.js +21 -0
  26. package/src/helpers/pager.js +59 -0
  27. package/src/helpers/pager.test.js +85 -0
  28. package/src/lib/README.md +77 -22
  29. package/src/lib/conf/README.md +6 -0
  30. package/src/lib/conf/build-status.js +103 -0
  31. package/src/lib/conf/build-status.test.js +247 -0
  32. package/src/lib/conf/defaults.js +14 -0
  33. package/src/lib/conf/format.js +1 -1
  34. package/src/lib/conf/schema.js +46 -1
  35. package/src/lib/constants.js +28 -0
  36. package/src/lib/doctor/README.md +667 -0
  37. package/src/lib/doctor/checks/environment/cli-version.js +62 -0
  38. package/src/lib/doctor/checks/environment/cli-version.test.js +132 -0
  39. package/src/lib/doctor/checks/environment/git-available.js +77 -0
  40. package/src/lib/doctor/checks/environment/git-available.test.js +52 -0
  41. package/src/lib/doctor/checks/environment/index.js +13 -0
  42. package/src/lib/doctor/checks/environment/node-version.js +66 -0
  43. package/src/lib/doctor/checks/environment/node-version.test.js +17 -0
  44. package/src/lib/doctor/checks/environment/npm-available.js +66 -0
  45. package/src/lib/doctor/checks/environment/npm-available.test.js +52 -0
  46. package/src/lib/doctor/checks/environment/os-info.js +48 -0
  47. package/src/lib/doctor/checks/environment/os-info.test.js +81 -0
  48. package/src/lib/doctor/checks/environment/runtime-info.js +51 -0
  49. package/src/lib/doctor/checks/environment/runtime-info.test.js +132 -0
  50. package/src/lib/doctor/checks/outputs/epub-output.js +119 -0
  51. package/src/lib/doctor/checks/outputs/epub-output.test.js +277 -0
  52. package/src/lib/doctor/checks/outputs/index.js +10 -0
  53. package/src/lib/doctor/checks/outputs/pdf-output.js +144 -0
  54. package/src/lib/doctor/checks/outputs/pdf-output.test.js +377 -0
  55. package/src/lib/doctor/checks/outputs/stale-build.js +122 -0
  56. package/src/lib/doctor/checks/outputs/stale-build.test.js +282 -0
  57. package/src/lib/doctor/checks/project/data-files.js +56 -0
  58. package/src/lib/doctor/checks/project/data-files.test.js +125 -0
  59. package/src/lib/doctor/checks/project/dependencies.js +53 -0
  60. package/src/lib/doctor/checks/project/dependencies.test.js +71 -0
  61. package/src/lib/doctor/checks/project/index.js +11 -0
  62. package/src/lib/doctor/checks/project/quire-11ty.js +98 -0
  63. package/src/lib/doctor/checks/project/quire-11ty.test.js +170 -0
  64. package/src/lib/doctor/checks/project/quire-project.js +38 -0
  65. package/src/lib/doctor/checks/project/quire-project.test.js +47 -0
  66. package/src/lib/doctor/checks/tools/index.js +10 -0
  67. package/src/lib/doctor/checks/tools/pandoc-available.js +82 -0
  68. package/src/lib/doctor/checks/tools/pandoc-available.test.js +73 -0
  69. package/src/lib/doctor/checks/tools/prince-available.js +81 -0
  70. package/src/lib/doctor/checks/tools/prince-available.test.js +73 -0
  71. package/src/lib/doctor/constants.js +39 -0
  72. package/src/lib/doctor/formatDuration.js +108 -0
  73. package/src/lib/doctor/formatDuration.test.js +76 -0
  74. package/src/lib/doctor/formatters/human.js +257 -0
  75. package/src/lib/doctor/formatters/human.test.js +463 -0
  76. package/src/lib/doctor/formatters/index.js +8 -0
  77. package/src/lib/doctor/formatters/json.js +78 -0
  78. package/src/lib/doctor/formatters/json.test.js +174 -0
  79. package/src/lib/doctor/formatters/shared.js +129 -0
  80. package/src/lib/doctor/formatters/shared.test.js +194 -0
  81. package/src/lib/doctor/index.js +271 -0
  82. package/src/lib/doctor/index.test.js +797 -0
  83. package/src/lib/help/frontmatter.js +77 -0
  84. package/src/lib/help/frontmatter.test.js +139 -0
  85. package/src/lib/help/index.js +135 -0
  86. package/src/lib/help/index.test.js +160 -0
  87. package/src/lib/help/topics/configuration.md +77 -0
  88. package/src/lib/help/topics/debugging.md +69 -0
  89. package/src/lib/help/topics/epub.md +74 -0
  90. package/src/lib/help/topics/pdf.md +74 -0
  91. package/src/lib/help/topics/publishing.md +80 -0
  92. package/src/lib/help/topics/workflows.md +50 -0
  93. package/src/lib/platform.js +95 -0
  94. package/src/lib/project/build.js +44 -25
  95. package/src/lib/project/build.test.js +30 -8
  96. package/src/lib/project/detect.js +16 -2
  97. package/src/lib/project/index.js +18 -2
  98. package/src/lib/project/output-paths.js +87 -0
  99. package/src/lib/project/output-paths.test.js +66 -0
  100. package/src/lib/project/paths.js +48 -0
  101. package/src/main.js +38 -3
  102. package/src/packageConfig.js +17 -0
  103. package/src/validators/validate-data-files.js +154 -0
  104. package/src/validators/validate-data-files.test.js +217 -0
@@ -23,16 +23,8 @@ test.beforeEach((t) => {
23
23
  // Mock testcwd helper
24
24
  t.context.mockTestcwd = t.context.sandbox.stub()
25
25
 
26
- // Mock execaCommand for external commands
27
- t.context.mockExecaCommand = t.context.sandbox.stub()
28
- t.context.mockExecaCommand.withArgs('quire --version').resolves({ stdout: '1.0.0-rc.33' })
29
- t.context.mockExecaCommand.withArgs('npm --version').resolves({ stdout: '10.2.4' })
30
-
31
- // Mock os module
32
- t.context.mockOs = {
33
- type: t.context.sandbox.stub().returns('Darwin'),
34
- release: t.context.sandbox.stub().returns('23.0.0')
35
- }
26
+ // Mock packageConfig binPath
27
+ t.context.mockBinPath = t.context.sandbox.stub().returns('/usr/local/bin/quire')
36
28
 
37
29
  // Mock fs module
38
30
  t.context.mockFs = {
@@ -72,16 +64,15 @@ test.afterEach.always((t) => {
72
64
  })
73
65
 
74
66
  test('info command should validate Quire project in preAction', async (t) => {
75
- const { sandbox, mockLogger, mockTestcwd, mockConfig, mockExecaCommand, mockOs, mockFs } = t.context
67
+ const { sandbox, mockLogger, mockTestcwd, mockConfig, mockBinPath, mockFs } = t.context
76
68
 
77
69
  const { default: InfoCommand } = await esmock('./info.js', {
78
70
  '#helpers/test-cwd.js': {
79
71
  default: mockTestcwd
80
72
  },
81
- 'execa': {
82
- execaCommand: mockExecaCommand
73
+ '#src/packageConfig.js': {
74
+ binPath: mockBinPath
83
75
  },
84
- 'node:os': mockOs,
85
76
  'node:fs': mockFs
86
77
  }, {
87
78
  '#lib/logger/index.js': {
@@ -108,16 +99,15 @@ test('info command should validate Quire project in preAction', async (t) => {
108
99
  })
109
100
 
110
101
  test('info command should display project version information', async (t) => {
111
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
102
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
112
103
 
113
104
  const { default: InfoCommand } = await esmock('./info.js', {
114
105
  '#helpers/test-cwd.js': {
115
106
  default: mockTestcwd
116
107
  },
117
- 'execa': {
118
- execaCommand: mockExecaCommand
108
+ '#src/packageConfig.js': {
109
+ binPath: mockBinPath
119
110
  },
120
- 'node:os': mockOs,
121
111
  'node:fs': mockFs
122
112
  }, {
123
113
  '#lib/logger/index.js': {
@@ -135,31 +125,42 @@ test('info command should display project version information', async (t) => {
135
125
  // Verify logger.info was called with project information
136
126
  t.true(mockLogger.info.called, 'logger.info should be called')
137
127
 
138
- // Check for project section header (uses current directory name)
128
+ // Check for project header
139
129
  const calls = mockLogger.info.getCalls()
140
130
  const hasProjectInfo = calls.some((call) =>
141
- call.args[0] && call.args[0].includes('[') && call.args[0].includes(']')
131
+ call.args[0] && call.args[0].includes('Project:')
142
132
  )
143
- t.true(hasProjectInfo, 'should display project section')
133
+ t.true(hasProjectInfo, 'should display project header')
144
134
 
145
135
  // Check for quire-cli version in output
146
136
  const hasCliVersion = calls.some((call) =>
147
137
  call.args[0] && call.args[0].includes('quire-cli') && call.args[0].includes('1.0.0-rc.33')
148
138
  )
149
139
  t.true(hasCliVersion, 'should display quire-cli version')
140
+
141
+ // Check for quire-11ty version in output
142
+ const has11tyVersion = calls.some((call) =>
143
+ call.args[0] && call.args[0].includes('quire-11ty')
144
+ )
145
+ t.true(has11tyVersion, 'should display quire-11ty version')
146
+
147
+ // Check for doctor tip
148
+ const hasDoctorTip = calls.some((call) =>
149
+ call.args[0] && call.args[0].includes('quire doctor')
150
+ )
151
+ t.true(hasDoctorTip, 'should display doctor tip')
150
152
  })
151
153
 
152
- test('info command should display system information with debug flag', async (t) => {
153
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
154
+ test('info command should display starter version when available', async (t) => {
155
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
154
156
 
155
157
  const { default: InfoCommand } = await esmock('./info.js', {
156
158
  '#helpers/test-cwd.js': {
157
159
  default: mockTestcwd
158
160
  },
159
- 'execa': {
160
- execaCommand: mockExecaCommand
161
+ '#src/packageConfig.js': {
162
+ binPath: mockBinPath
161
163
  },
162
- 'node:os': mockOs,
163
164
  'node:fs': mockFs
164
165
  }, {
165
166
  '#lib/logger/index.js': {
@@ -172,42 +173,63 @@ test('info command should display system information with debug flag', async (t)
172
173
  command.logger = mockLogger
173
174
  command.debug = sandbox.stub()
174
175
 
175
- await command.action({ debug: true }, {})
176
+ await command.action({}, {})
176
177
 
177
178
  // Verify logger.info was called
178
179
  t.true(mockLogger.info.called, 'logger.info should be called')
179
180
 
180
- // Check for system section header
181
+ // Check for starter version in output (from version file)
181
182
  const calls = mockLogger.info.getCalls()
182
- const hasSystemInfo = calls.some((call) =>
183
- call.args[0] && call.args[0].includes('[System]')
183
+ const hasStarterVersion = calls.some((call) =>
184
+ call.args[0] && call.args[0].includes('starter')
184
185
  )
185
- t.true(hasSystemInfo, 'should display system section with debug flag')
186
+ t.true(hasStarterVersion, 'should display starter version when available')
187
+ })
186
188
 
187
- // Check for node version in output (only shown with debug)
188
- const hasNodeVersion = calls.some((call) =>
189
- call.args[0] && call.args[0].includes('node')
190
- )
191
- t.true(hasNodeVersion, 'should display node version with debug flag')
189
+ test('info command --debug shows installation paths', async (t) => {
190
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
192
191
 
193
- // Check for npm version in output (only shown with debug)
194
- const hasNpmVersion = calls.some((call) =>
195
- call.args[0] && call.args[0].includes('npm')
196
- )
197
- t.true(hasNpmVersion, 'should display npm version with debug flag')
192
+ const { default: InfoCommand } = await esmock('./info.js', {
193
+ '#helpers/test-cwd.js': {
194
+ default: mockTestcwd
195
+ },
196
+ '#src/packageConfig.js': {
197
+ binPath: mockBinPath
198
+ },
199
+ 'node:fs': mockFs
200
+ }, {
201
+ '#lib/logger/index.js': {
202
+ default: () => mockLogger
203
+ }
204
+ })
205
+
206
+ const command = new InfoCommand()
207
+ command.config = mockConfig
208
+ command.logger = mockLogger
209
+ command.debug = sandbox.stub()
210
+
211
+ await command.action({ debug: true }, {})
212
+
213
+ const calls = mockLogger.info.getCalls()
214
+ const output = calls.map((call) => call.args[0]).join('\n')
215
+
216
+ // Check for quire-cli path
217
+ t.true(output.includes('/usr/local/bin/quire'), 'should display quire-cli path')
198
218
  })
199
219
 
200
- test('info command should hide system details without debug flag', async (t) => {
201
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
220
+ test('info command --debug handles missing quire-cli in PATH', async (t) => {
221
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockFs } = t.context
222
+
223
+ // binPath returns null when executable is not found
224
+ const mockBinPathNotFound = sandbox.stub().returns(null)
202
225
 
203
226
  const { default: InfoCommand } = await esmock('./info.js', {
204
227
  '#helpers/test-cwd.js': {
205
228
  default: mockTestcwd
206
229
  },
207
- 'execa': {
208
- execaCommand: mockExecaCommand
230
+ '#src/packageConfig.js': {
231
+ binPath: mockBinPathNotFound
209
232
  },
210
- 'node:os': mockOs,
211
233
  'node:fs': mockFs
212
234
  }, {
213
235
  '#lib/logger/index.js': {
@@ -220,23 +242,16 @@ test('info command should hide system details without debug flag', async (t) =>
220
242
  command.logger = mockLogger
221
243
  command.debug = sandbox.stub()
222
244
 
223
- await command.action({}, {})
224
-
225
- // Verify logger.info was called
226
- t.true(mockLogger.info.called, 'logger.info should be called')
245
+ await command.action({ debug: true }, {})
227
246
 
228
- // Check that system details are NOT shown without debug flag
229
247
  const calls = mockLogger.info.getCalls()
248
+ const output = calls.map((call) => call.args[0]).join('\n')
230
249
 
231
- // Node version should not appear in non-debug output
232
- const hasNodeVersion = calls.some((call) =>
233
- call.args[0] && call.args[0].toLowerCase().includes('node') && !call.args[0].includes('quire')
234
- )
235
- t.false(hasNodeVersion, 'should not display node version without debug flag')
250
+ t.true(output.includes('not found in PATH'), 'should show not found message for quire-cli')
236
251
  })
237
252
 
238
253
  test('info command should handle missing version file', async (t) => {
239
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
254
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
240
255
 
241
256
  // Setup mockFs to throw error for missing version file (simulating file not found)
242
257
  mockFs.readFileSync.callsFake((filePath, options) => {
@@ -253,10 +268,9 @@ test('info command should handle missing version file', async (t) => {
253
268
  '#helpers/test-cwd.js': {
254
269
  default: mockTestcwd
255
270
  },
256
- 'execa': {
257
- execaCommand: mockExecaCommand
271
+ '#src/packageConfig.js': {
272
+ binPath: mockBinPath
258
273
  },
259
- 'node:os': mockOs,
260
274
  'node:fs': mockFs
261
275
  }, {
262
276
  '#lib/logger/index.js': {
@@ -283,7 +297,7 @@ test('info command should handle missing version file', async (t) => {
283
297
  })
284
298
 
285
299
  test('info command should handle malformed version file', async (t) => {
286
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
300
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
287
301
 
288
302
  // Setup mockFs to return malformed JSON (simulating corrupted file)
289
303
  mockFs.readFileSync.callsFake((filePath, options) => {
@@ -300,10 +314,9 @@ test('info command should handle malformed version file', async (t) => {
300
314
  '#helpers/test-cwd.js': {
301
315
  default: mockTestcwd
302
316
  },
303
- 'execa': {
304
- execaCommand: mockExecaCommand
317
+ '#src/packageConfig.js': {
318
+ binPath: mockBinPath
305
319
  },
306
- 'node:os': mockOs,
307
320
  'node:fs': mockFs
308
321
  }, {
309
322
  '#lib/logger/index.js': {
@@ -333,7 +346,7 @@ test('info command should handle malformed version file', async (t) => {
333
346
  })
334
347
 
335
348
  test('info command should read quire-11ty version from package.json', async (t) => {
336
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
349
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
337
350
 
338
351
  // Override package.json to have specific version
339
352
  mockFs.readFileSync.callsFake((filePath, options) => {
@@ -353,10 +366,9 @@ test('info command should read quire-11ty version from package.json', async (t)
353
366
  '#helpers/test-cwd.js': {
354
367
  default: mockTestcwd
355
368
  },
356
- 'execa': {
357
- execaCommand: mockExecaCommand
369
+ '#src/packageConfig.js': {
370
+ binPath: mockBinPath
358
371
  },
359
- 'node:os': mockOs,
360
372
  'node:fs': mockFs
361
373
  }, {
362
374
  '#lib/logger/index.js': {
@@ -382,8 +394,8 @@ test('info command should read quire-11ty version from package.json', async (t)
382
394
  t.true(hasEleventyVersion, 'should display quire-11ty version from package.json')
383
395
  })
384
396
 
385
- test('info command should log debug information when debug flag is set', async (t) => {
386
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
397
+ test('info command should log debug information', async (t) => {
398
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
387
399
 
388
400
  const mockDebug = sandbox.stub()
389
401
 
@@ -391,10 +403,9 @@ test('info command should log debug information when debug flag is set', async (
391
403
  '#helpers/test-cwd.js': {
392
404
  default: mockTestcwd
393
405
  },
394
- 'execa': {
395
- execaCommand: mockExecaCommand
406
+ '#src/packageConfig.js': {
407
+ binPath: mockBinPath
396
408
  },
397
- 'node:os': mockOs,
398
409
  'node:fs': mockFs
399
410
  }, {
400
411
  '#lib/logger/index.js': {
@@ -407,9 +418,95 @@ test('info command should log debug information when debug flag is set', async (
407
418
  command.logger = mockLogger
408
419
  command.debug = mockDebug
409
420
 
410
- await command.action({ debug: true }, {})
421
+ await command.action({}, {})
411
422
 
412
423
  // Verify debug was called
413
- t.true(mockDebug.called, 'debug should be called with debug flag')
414
- t.true(mockDebug.calledWith('called with options %O', { debug: true }))
424
+ t.true(mockDebug.called, 'debug should be called')
425
+ t.true(mockDebug.calledWith('called with options %O', {}))
426
+ })
427
+
428
+ // ─────────────────────────────────────────────────────────────────────────────
429
+ // JSON output tests
430
+ // ─────────────────────────────────────────────────────────────────────────────
431
+
432
+ test.serial('info --json should output valid JSON with project versions', async (t) => {
433
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
434
+ const consoleLogStub = sandbox.stub(console, 'log')
435
+
436
+ const { default: InfoCommand } = await esmock('./info.js', {
437
+ '#helpers/test-cwd.js': {
438
+ default: mockTestcwd
439
+ },
440
+ '#src/packageConfig.js': {
441
+ binPath: mockBinPath
442
+ },
443
+ 'node:fs': mockFs
444
+ }, {
445
+ '#lib/logger/index.js': {
446
+ default: () => mockLogger
447
+ }
448
+ })
449
+
450
+ const command = new InfoCommand()
451
+ command.config = mockConfig
452
+ command.logger = mockLogger
453
+ command.debug = sandbox.stub()
454
+
455
+ await command.action({ json: true }, {})
456
+
457
+ // Should output via console.log, not logger.info
458
+ t.true(consoleLogStub.calledOnce, 'console.log should be called once')
459
+ t.false(mockLogger.info.called, 'logger.info should not be called in JSON mode')
460
+
461
+ // Parse and validate the JSON structure
462
+ const output = JSON.parse(consoleLogStub.firstCall.args[0])
463
+
464
+ t.truthy(output.project, 'JSON should have project section')
465
+
466
+ // Project section
467
+ t.is(output.project.cli, '1.0.0-rc.33')
468
+ t.is(output.project.quire11ty, '1.0.0')
469
+ t.is(output.project.starter, 'https://github.com/thegetty/quire-starter-default')
470
+ t.is(typeof output.project.directory, 'string')
471
+ })
472
+
473
+ test.serial('info --json should set starter to null when not available', async (t) => {
474
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
475
+ const consoleLogStub = sandbox.stub(console, 'log')
476
+
477
+ // Version file without starter
478
+ t.context.mockFs.readFileSync.callsFake((filePath, options) => {
479
+ if (filePath === './.quire-version') {
480
+ return JSON.stringify({ cli: '1.0.0-rc.33' })
481
+ }
482
+ if (filePath === './package.json') {
483
+ return t.context.packageJsonContent
484
+ }
485
+ return ''
486
+ })
487
+
488
+ const { default: InfoCommand } = await esmock('./info.js', {
489
+ '#helpers/test-cwd.js': {
490
+ default: mockTestcwd
491
+ },
492
+ '#src/packageConfig.js': {
493
+ binPath: mockBinPath
494
+ },
495
+ 'node:fs': mockFs
496
+ }, {
497
+ '#lib/logger/index.js': {
498
+ default: () => mockLogger
499
+ }
500
+ })
501
+
502
+ const command = new InfoCommand()
503
+ command.config = mockConfig
504
+ command.logger = mockLogger
505
+ command.debug = sandbox.stub()
506
+
507
+ await command.action({ json: true }, {})
508
+
509
+ const output = JSON.parse(consoleLogStub.firstCall.args[0])
510
+
511
+ t.is(output.project.starter, null, 'starter should be null when not in version file')
415
512
  })
@@ -5,6 +5,7 @@ import eleventy from '#lib/11ty/index.js'
5
5
  import generatePdf, { ENGINES } from '#lib/pdf/index.js'
6
6
  import open from 'open'
7
7
  import path from 'node:path'
8
+ import { recordStatus } from '#lib/conf/build-status.js'
8
9
  import reporter from '#lib/reporter/index.js'
9
10
  import testcwd from '#helpers/test-cwd.js'
10
11
  import { MissingBuildOutputError } from '#src/errors/index.js'
@@ -81,10 +82,16 @@ Examples:
81
82
 
82
83
  // Generate PDF - façade handles validation, progress, and errors
83
84
  const pdfOptions = { ...options, lib: options.engine }
84
- const output = await generatePdf(pdfOptions)
85
85
 
86
- if (options.open) {
87
- open(output)
86
+ try {
87
+ const output = await generatePdf(pdfOptions)
88
+ recordStatus(paths.getProjectRoot(), 'pdf', 'ok')
89
+ if (options.open) {
90
+ open(output)
91
+ }
92
+ } catch (error) {
93
+ recordStatus(paths.getProjectRoot(), 'pdf', 'failed')
94
+ throw error
88
95
  }
89
96
  }
90
97
 
@@ -25,11 +25,14 @@ export default class ValidateCommand extends Command {
25
25
  Examples:
26
26
  quire validate Check YAML syntax in content/_data/
27
27
  quire validate --verbose Validate with detailed file listing
28
+ quire validate --json Output validation results as JSON
28
29
 
29
30
  Validates YAML files in content/_data/ directory.
30
31
  `,
31
32
  version: '1.0.0',
32
- options: [],
33
+ options: [
34
+ ['--json', 'output validation results as JSON'],
35
+ ],
33
36
  })
34
37
 
35
38
  constructor() {
@@ -45,21 +48,50 @@ Validates YAML files in content/_data/ directory.
45
48
  .map(file => path.join(dataPath, file)
46
49
  )
47
50
 
48
- let errorList = []
49
- this.logger.info('Validating YAML files..')
51
+ const results = []
52
+ if (!options.json) {
53
+ this.logger.info('Validating YAML files..')
54
+ }
50
55
 
51
56
  for (const file of files) {
52
57
  try {
53
58
  yamlValidation(file)
59
+ results.push({ file, status: 'passed' })
54
60
  } catch (error){
55
- errorList.push(error)
61
+ results.push({ file, status: 'failed', error: error.reason })
62
+ }
63
+ }
64
+
65
+ const errors = results.filter((r) => r.status === 'failed')
66
+
67
+ if (options.json) {
68
+ const output = {
69
+ summary: {
70
+ files: results.length,
71
+ passed: results.length - errors.length,
72
+ failed: errors.length,
73
+ },
74
+ files: results,
75
+ }
76
+ console.log(JSON.stringify(output, null, 2))
77
+
78
+ // Still throw so exit code reflects failure
79
+ if (errors.length > 0) {
80
+ throw new ValidationError(
81
+ `Validation failed with ${errors.length} error(s)`,
82
+ {
83
+ code: 'VALIDATION_FAILED',
84
+ suggestion: 'Fix the errors listed above and run validation again'
85
+ }
86
+ )
56
87
  }
88
+ return
57
89
  }
58
90
 
59
- if(errorList.length > 0) {
60
- errorList.forEach(err => { this.logger.error(`${err.reason}`) })
91
+ if (errors.length > 0) {
92
+ errors.forEach((r) => { this.logger.error(`${r.error}`) })
61
93
  throw new ValidationError(
62
- `Validation failed with ${errorList.length} error(s)`,
94
+ `Validation failed with ${errors.length} error(s)`,
63
95
  {
64
96
  code: 'VALIDATION_FAILED',
65
97
  suggestion: 'Fix the errors listed above and run validation again'
@@ -40,21 +40,28 @@ test('registered command has correct options', (t) => {
40
40
  const { command } = t.context
41
41
 
42
42
  // Get all options
43
+ const jsonOption = command.options.find((opt) => opt.long === '--json')
43
44
  const quietOption = command.options.find((opt) => opt.long === '--quiet')
44
45
  const verboseOption = command.options.find((opt) => opt.long === '--verbose')
45
46
  const debugOption = command.options.find((opt) => opt.long === '--debug')
46
47
 
47
48
  // Verify all options exist
49
+ t.truthy(jsonOption, '--json option should exist')
48
50
  t.truthy(quietOption, '--quiet option should exist')
49
51
  t.truthy(verboseOption, '--verbose option should exist')
50
52
  t.truthy(debugOption, '--debug option should exist')
51
53
 
52
54
  // Verify they are Option instances
55
+ t.true(jsonOption instanceof Option, '--json should be Option instance')
53
56
  t.true(quietOption instanceof Option, '--quiet should be Option instance')
54
57
  t.true(verboseOption instanceof Option, '--verbose should be Option instance')
55
58
  t.true(debugOption instanceof Option, '--debug should be Option instance')
56
59
 
57
60
  // Verify option properties
61
+ t.is(jsonOption.long, '--json')
62
+ t.truthy(jsonOption.description)
63
+ t.false(jsonOption.required, '--json should not require a value')
64
+
58
65
  t.is(quietOption.long, '--quiet')
59
66
  t.is(quietOption.short, '-q')
60
67
  t.truthy(quietOption.description)
@@ -76,6 +83,7 @@ test('command options are accessible via public API', (t) => {
76
83
  // Test that options can be accessed the way Commander.js does
77
84
  const optionNames = command.options.map((opt) => opt.long)
78
85
 
86
+ t.true(optionNames.includes('--json'))
79
87
  t.true(optionNames.includes('--quiet'))
80
88
  t.true(optionNames.includes('--verbose'))
81
89
  t.true(optionNames.includes('--debug'))