@thegetty/quire-cli 1.0.0-rc.34 → 1.0.0-rc.36

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 (159) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -0
  3. package/bin/cli.js +19 -1
  4. package/package.json +21 -8
  5. package/schemas/config.schema.json +194 -0
  6. package/schemas/figures.schema.json +56 -0
  7. package/schemas/layout.schema.json +5 -0
  8. package/schemas/objects.schema.json +62 -0
  9. package/schemas/publication.schema.json +140 -0
  10. package/schemas/references.schema.json +29 -0
  11. package/src/Command.js +26 -6
  12. package/src/Command.spec.js +99 -0
  13. package/src/commands/README.md +213 -122
  14. package/src/commands/build.js +46 -37
  15. package/src/commands/build.spec.js +113 -0
  16. package/src/commands/build.test.js +402 -0
  17. package/src/commands/clean.js +25 -19
  18. package/src/commands/clean.spec.js +108 -0
  19. package/src/commands/clean.test.js +260 -0
  20. package/src/commands/config.js +251 -0
  21. package/src/commands/config.spec.js +107 -0
  22. package/src/commands/config.test.js +715 -0
  23. package/src/commands/create.js +42 -14
  24. package/src/commands/create.spec.js +112 -0
  25. package/src/commands/create.test.js +415 -0
  26. package/src/commands/epub.js +59 -30
  27. package/src/commands/epub.spec.js +114 -0
  28. package/src/commands/epub.test.js +503 -0
  29. package/src/commands/index.js +9 -2
  30. package/src/commands/info.js +18 -24
  31. package/src/commands/info.spec.js +64 -0
  32. package/src/commands/info.test.js +415 -0
  33. package/src/commands/pdf.js +58 -84
  34. package/src/commands/pdf.spec.js +114 -0
  35. package/src/commands/pdf.test.js +464 -0
  36. package/src/commands/preview.js +26 -31
  37. package/src/commands/preview.spec.js +97 -0
  38. package/src/commands/preview.test.js +250 -0
  39. package/src/commands/use.js +56 -0
  40. package/src/commands/use.spec.js +61 -0
  41. package/src/commands/use.test.js +280 -0
  42. package/src/commands/validate.js +31 -19
  43. package/src/commands/validate.spec.js +82 -0
  44. package/src/commands/validate.test.js +234 -0
  45. package/src/commands/workflows.js +70 -0
  46. package/src/errors/build/build-failed-error.js +19 -0
  47. package/src/errors/build/config-field-missing-error.js +20 -0
  48. package/src/errors/build/config-file-not-found-error.js +20 -0
  49. package/src/errors/build/index.js +11 -0
  50. package/src/errors/index.js +48 -0
  51. package/src/errors/install/dependency-install-error.js +19 -0
  52. package/src/errors/install/directory-not-empty-error.js +25 -0
  53. package/src/errors/install/index.js +12 -0
  54. package/src/errors/install/invalid-path-error.js +31 -0
  55. package/src/errors/install/invalid-starter-error.js +27 -0
  56. package/src/errors/install/version-not-found-error.js +35 -0
  57. package/src/errors/output/epub-generation-error.js +19 -0
  58. package/src/errors/output/index.js +14 -0
  59. package/src/errors/output/invalid-epub-library-error.js +20 -0
  60. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  61. package/src/errors/output/missing-build-output-error.js +21 -0
  62. package/src/errors/output/pdf-generation-error.js +24 -0
  63. package/src/errors/output/tool-not-found-error.js +37 -0
  64. package/src/errors/project/index.js +10 -0
  65. package/src/errors/project/not-in-project-error.js +20 -0
  66. package/src/errors/project/project-create-error.js +21 -0
  67. package/src/errors/quire-error.js +27 -0
  68. package/src/errors/validation/validation-error.js +20 -11
  69. package/src/helpers/clean.js +1 -1
  70. package/src/helpers/docs-url.js +32 -0
  71. package/src/helpers/test-cwd.js +5 -6
  72. package/src/helpers/test-cwd.test.js +192 -0
  73. package/src/helpers/which.js +10 -4
  74. package/src/lib/11ty/README.md +135 -19
  75. package/src/lib/11ty/api.js +176 -93
  76. package/src/lib/11ty/cli.js +77 -35
  77. package/src/lib/11ty/index.js +64 -5
  78. package/src/lib/11ty/index.test.js +655 -0
  79. package/src/lib/README.md +275 -0
  80. package/src/lib/commander/index.js +100 -0
  81. package/src/lib/commander/index.test.js +86 -0
  82. package/src/lib/commander/options.js +195 -0
  83. package/src/lib/commander/options.test.js +109 -0
  84. package/src/lib/conf/README.md +84 -73
  85. package/src/lib/conf/config.js +5 -3
  86. package/src/lib/conf/config.test.js +281 -0
  87. package/src/lib/conf/defaults.js +44 -0
  88. package/src/lib/conf/format.js +60 -0
  89. package/src/lib/conf/format.test.js +106 -0
  90. package/src/lib/conf/helpers.js +91 -0
  91. package/src/lib/conf/helpers.test.js +136 -0
  92. package/src/lib/conf/index.js +22 -0
  93. package/src/lib/conf/schema.js +53 -8
  94. package/src/lib/epub/README.md +133 -2
  95. package/src/lib/epub/engines.js +46 -0
  96. package/src/lib/epub/epub.js +36 -11
  97. package/src/lib/epub/index.js +111 -21
  98. package/src/lib/epub/index.test.js +518 -0
  99. package/src/lib/epub/pandoc.js +33 -4
  100. package/src/lib/epub/pandoc.test.js +122 -0
  101. package/src/lib/epub/schema.js +21 -0
  102. package/src/lib/error/README.md +170 -0
  103. package/src/lib/error/handler.js +107 -0
  104. package/src/lib/git/README.md +151 -2
  105. package/src/lib/git/index.js +217 -13
  106. package/src/lib/git/index.spec.js +80 -0
  107. package/src/lib/git/index.test.js +453 -0
  108. package/src/lib/installer/index.js +309 -0
  109. package/src/lib/installer/index.spec.js +83 -0
  110. package/src/lib/installer/index.test.js +545 -0
  111. package/src/lib/logger/README.md +424 -0
  112. package/src/lib/logger/debug.js +90 -0
  113. package/src/lib/logger/debug.spec.js +130 -0
  114. package/src/lib/logger/index.js +228 -0
  115. package/src/lib/logger/index.spec.js +131 -0
  116. package/src/lib/logger/index.test.js +477 -0
  117. package/src/lib/npm/README.md +127 -0
  118. package/src/lib/npm/index.js +198 -0
  119. package/src/lib/npm/index.spec.js +60 -0
  120. package/src/lib/npm/index.test.js +355 -0
  121. package/src/lib/pdf/README.md +131 -0
  122. package/src/lib/pdf/engines.js +46 -0
  123. package/src/lib/pdf/index.js +124 -21
  124. package/src/lib/pdf/index.test.js +708 -0
  125. package/src/lib/pdf/paged.js +100 -52
  126. package/src/lib/pdf/paged.test.js +366 -0
  127. package/src/lib/pdf/prince.js +115 -37
  128. package/src/lib/pdf/prince.test.js +202 -0
  129. package/src/lib/pdf/schema.js +21 -0
  130. package/src/lib/pdf/split.js +61 -33
  131. package/src/lib/pdf/split.test.js +445 -0
  132. package/src/lib/process/manager.js +110 -0
  133. package/src/lib/process/manager.test.js +55 -0
  134. package/src/lib/project/build.js +143 -0
  135. package/src/lib/project/build.test.js +253 -0
  136. package/src/lib/project/config.js +48 -0
  137. package/src/lib/project/config.test.js +134 -0
  138. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  139. package/src/lib/project/detect.test.js +157 -0
  140. package/src/lib/project/index.js +40 -0
  141. package/src/lib/project/paths.js +224 -0
  142. package/src/lib/project/version.js +110 -0
  143. package/src/lib/project/version.test.js +350 -0
  144. package/src/lib/reporter/README.md +211 -2
  145. package/src/lib/reporter/index.js +512 -0
  146. package/src/lib/reporter/index.test.js +593 -0
  147. package/src/main.js +134 -47
  148. package/src/main.spec.js +61 -0
  149. package/src/main.test.js +347 -0
  150. package/src/validators/utils.js +2 -1
  151. package/src/commands/conf.js +0 -43
  152. package/src/commands/version.js +0 -43
  153. package/src/lib/11ty/paths.js +0 -103
  154. package/src/lib/i18n/README.md +0 -3
  155. package/src/lib/i18n/config.js +0 -53
  156. package/src/lib/i18n/index.js +0 -43
  157. package/src/lib/i18n/localeService.js +0 -58
  158. package/src/lib/quire/README.md +0 -19
  159. package/src/lib/quire/index.js +0 -350
@@ -0,0 +1,503 @@
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 reporter
9
+ t.context.mockReporter = {
10
+ configure: t.context.sandbox.stub().returnsThis(),
11
+ start: t.context.sandbox.stub().returnsThis(),
12
+ update: t.context.sandbox.stub().returnsThis(),
13
+ succeed: t.context.sandbox.stub().returnsThis(),
14
+ fail: t.context.sandbox.stub().returnsThis(),
15
+ stop: t.context.sandbox.stub().returnsThis(),
16
+ }
17
+ })
18
+
19
+ test.afterEach.always((t) => {
20
+ t.context.sandbox.restore()
21
+ })
22
+
23
+ test('epub command should call generateEpub with epubjs engine', async (t) => {
24
+ const { sandbox, mockReporter } = t.context
25
+
26
+ const mockGenerateEpub = sandbox.stub().resolves('/project/epubjs.epub')
27
+
28
+ const EPUBCommand = await esmock('./epub.js', {
29
+ '#lib/epub/index.js': {
30
+ default: mockGenerateEpub
31
+ },
32
+ '#lib/project/index.js': {
33
+ hasEpubOutput: () => true
34
+ },
35
+ '#lib/reporter/index.js': {
36
+ default: mockReporter
37
+ },
38
+ 'fs-extra': {
39
+ existsSync: () => true
40
+ },
41
+ open: {
42
+ default: sandbox.stub()
43
+ }
44
+ })
45
+
46
+ const command = new EPUBCommand()
47
+ command.name = sandbox.stub().returns('epub')
48
+
49
+ await command.action({ engine: 'epubjs' }, command)
50
+
51
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
52
+ t.is(mockGenerateEpub.firstCall.args[0].lib, 'epubjs', 'should pass lib to generateEpub')
53
+ // Nota bene: reporter.start/succeed are called by façade (lib/epub/index.js), not the command
54
+ })
55
+
56
+ test('epub command should call generateEpub with pandoc engine', async (t) => {
57
+ const { sandbox, mockReporter } = t.context
58
+
59
+ const mockGenerateEpub = sandbox.stub().resolves('/project/pandoc.epub')
60
+
61
+ const EPUBCommand = await esmock('./epub.js', {
62
+ '#lib/epub/index.js': {
63
+ default: mockGenerateEpub
64
+ },
65
+ '#lib/project/index.js': {
66
+ hasEpubOutput: () => true
67
+ },
68
+ '#lib/reporter/index.js': {
69
+ default: mockReporter
70
+ },
71
+ 'fs-extra': {
72
+ existsSync: () => true
73
+ },
74
+ open: {
75
+ default: sandbox.stub()
76
+ }
77
+ })
78
+
79
+ const command = new EPUBCommand()
80
+ command.name = sandbox.stub().returns('epub')
81
+
82
+ await command.action({ engine: 'pandoc' }, command)
83
+
84
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
85
+ t.is(mockGenerateEpub.firstCall.args[0].lib, 'pandoc', 'should pass lib to generateEpub')
86
+ })
87
+
88
+ test('epub command should open EPUB when --open flag is provided', async (t) => {
89
+ const { sandbox, mockReporter } = t.context
90
+
91
+ const outputPath = '/project/epubjs.epub'
92
+ const mockGenerateEpub = sandbox.stub().resolves(outputPath)
93
+ const mockOpen = sandbox.stub()
94
+
95
+ const EPUBCommand = await esmock('./epub.js', {
96
+ '#lib/epub/index.js': {
97
+ default: mockGenerateEpub
98
+ },
99
+ '#lib/project/index.js': {
100
+ hasEpubOutput: () => true
101
+ },
102
+ '#lib/reporter/index.js': {
103
+ default: mockReporter
104
+ },
105
+ 'fs-extra': {
106
+ existsSync: () => true
107
+ },
108
+ open: {
109
+ default: mockOpen
110
+ }
111
+ })
112
+
113
+ const command = new EPUBCommand()
114
+ command.name = sandbox.stub().returns('epub')
115
+
116
+ await command.action({ engine: 'epubjs', open: true }, command)
117
+
118
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
119
+ t.true(mockOpen.called, 'open should be called when --open flag is provided')
120
+ t.is(mockOpen.firstCall.args[0], outputPath, 'should open the generated EPUB path')
121
+ })
122
+
123
+ test('epub command should not open EPUB when --open flag is not provided', async (t) => {
124
+ const { sandbox, mockReporter } = t.context
125
+
126
+ const mockGenerateEpub = sandbox.stub().resolves('/project/epubjs.epub')
127
+ const mockOpen = sandbox.stub()
128
+
129
+ const EPUBCommand = await esmock('./epub.js', {
130
+ '#lib/epub/index.js': {
131
+ default: mockGenerateEpub
132
+ },
133
+ '#lib/project/index.js': {
134
+ hasEpubOutput: () => true
135
+ },
136
+ '#lib/reporter/index.js': {
137
+ default: mockReporter
138
+ },
139
+ 'fs-extra': {
140
+ existsSync: () => true
141
+ },
142
+ open: {
143
+ default: mockOpen
144
+ }
145
+ })
146
+
147
+ const command = new EPUBCommand()
148
+ command.name = sandbox.stub().returns('epub')
149
+
150
+ await command.action({ engine: 'epubjs' }, command)
151
+
152
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
153
+ t.false(mockOpen.called, 'open should not be called without --open flag')
154
+ })
155
+
156
+ test('epub command should pass debug option to generateEpub', async (t) => {
157
+ const { sandbox, mockReporter } = t.context
158
+
159
+ const mockGenerateEpub = sandbox.stub().resolves('/project/epubjs.epub')
160
+
161
+ const EPUBCommand = await esmock('./epub.js', {
162
+ '#lib/epub/index.js': {
163
+ default: mockGenerateEpub
164
+ },
165
+ '#lib/project/index.js': {
166
+ hasEpubOutput: () => true
167
+ },
168
+ '#lib/reporter/index.js': {
169
+ default: mockReporter
170
+ },
171
+ 'fs-extra': {
172
+ existsSync: () => true
173
+ },
174
+ open: {
175
+ default: sandbox.stub()
176
+ }
177
+ })
178
+
179
+ const command = new EPUBCommand()
180
+ command.name = sandbox.stub().returns('epub')
181
+
182
+ await command.action({ engine: 'epubjs', debug: true }, command)
183
+
184
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
185
+ t.is(mockGenerateEpub.firstCall.args[0].lib, 'epubjs', 'should pass lib to generateEpub')
186
+ t.true(mockGenerateEpub.firstCall.args[0].debug, 'should pass debug option')
187
+ })
188
+
189
+ test('epub command should throw MissingBuildOutputError when build output is missing', async (t) => {
190
+ const { sandbox, mockReporter } = t.context
191
+
192
+ const mockGenerateEpub = sandbox.stub().resolves('/project/epubjs.epub')
193
+
194
+ const EPUBCommand = await esmock('./epub.js', {
195
+ '#lib/epub/index.js': {
196
+ default: mockGenerateEpub
197
+ },
198
+ '#lib/project/index.js': {
199
+ default: { getEpubDir: () => '_epub' },
200
+ hasEpubOutput: () => false
201
+ },
202
+ '#lib/reporter/index.js': {
203
+ default: mockReporter
204
+ },
205
+ 'fs-extra': {
206
+ existsSync: () => true
207
+ },
208
+ open: {
209
+ default: sandbox.stub()
210
+ }
211
+ })
212
+
213
+ const command = new EPUBCommand()
214
+ command.name = sandbox.stub().returns('epub')
215
+
216
+ const error = await t.throwsAsync(() => command.action({ engine: 'epubjs' }, command))
217
+
218
+ t.is(error.code, 'BUILD_OUTPUT_MISSING', 'should throw BUILD_OUTPUT_MISSING error')
219
+ t.regex(error.message, /EPUB/, 'error should mention EPUB')
220
+ t.false(mockGenerateEpub.called, 'generateEpub should not be called when build output is missing')
221
+ })
222
+
223
+ test('epub command should run build first when --build flag is set and output missing', async (t) => {
224
+ const { sandbox, mockReporter } = t.context
225
+
226
+ const mockGenerateEpub = sandbox.stub().resolves('/project/epubjs.epub')
227
+ const mockBuild = sandbox.stub().resolves()
228
+ let buildCalled = false
229
+
230
+ const EPUBCommand = await esmock('./epub.js', {
231
+ '#lib/epub/index.js': {
232
+ default: mockGenerateEpub
233
+ },
234
+ '#lib/project/index.js': {
235
+ hasEpubOutput: () => {
236
+ // Return false first (before build), true after build
237
+ if (!buildCalled) return false
238
+ return true
239
+ }
240
+ },
241
+ '#lib/11ty/index.js': {
242
+ default: {
243
+ build: async (opts) => {
244
+ buildCalled = true
245
+ return mockBuild(opts)
246
+ }
247
+ }
248
+ },
249
+ '#lib/reporter/index.js': {
250
+ default: mockReporter
251
+ },
252
+ 'fs-extra': {
253
+ existsSync: () => true
254
+ },
255
+ open: {
256
+ default: sandbox.stub()
257
+ }
258
+ })
259
+
260
+ const command = new EPUBCommand()
261
+ command.name = sandbox.stub().returns('epub')
262
+
263
+ await command.action({ engine: 'epubjs', build: true }, command)
264
+
265
+ t.true(mockBuild.called, 'build should be called when --build flag is set')
266
+ t.true(mockGenerateEpub.called, 'generateEpub should be called after build')
267
+ })
268
+
269
+ test('epub command should support deprecated --lib option', async (t) => {
270
+ const { sandbox, mockReporter } = t.context
271
+
272
+ const mockGenerateEpub = sandbox.stub().resolves('/project/pandoc.epub')
273
+
274
+ const EPUBCommand = await esmock('./epub.js', {
275
+ '#lib/epub/index.js': {
276
+ default: mockGenerateEpub
277
+ },
278
+ '#lib/project/index.js': {
279
+ hasEpubOutput: () => true
280
+ },
281
+ '#lib/reporter/index.js': {
282
+ default: mockReporter
283
+ },
284
+ 'fs-extra': {
285
+ existsSync: () => true
286
+ },
287
+ open: {
288
+ default: sandbox.stub()
289
+ }
290
+ })
291
+
292
+ const command = new EPUBCommand()
293
+ command.name = sandbox.stub().returns('epub')
294
+
295
+ // Use deprecated --lib option (should still work)
296
+ await command.action({ lib: 'pandoc' }, command)
297
+
298
+ t.true(mockGenerateEpub.called, 'generateEpub should be called with deprecated --lib')
299
+ t.is(mockGenerateEpub.firstCall.args[0].lib, 'pandoc', 'should pass lib to generateEpub from deprecated option')
300
+ })
301
+
302
+ test('epub command should propagate errors from generateEpub', async (t) => {
303
+ const { sandbox, mockReporter } = t.context
304
+
305
+ const epubError = new Error('EPUB generation failed')
306
+ const mockGenerateEpub = sandbox.stub().rejects(epubError)
307
+
308
+ const EPUBCommand = await esmock('./epub.js', {
309
+ '#lib/epub/index.js': {
310
+ default: mockGenerateEpub
311
+ },
312
+ '#lib/project/index.js': {
313
+ hasEpubOutput: () => true
314
+ },
315
+ '#lib/reporter/index.js': {
316
+ default: mockReporter
317
+ },
318
+ 'fs-extra': {
319
+ existsSync: () => true
320
+ },
321
+ open: {
322
+ default: sandbox.stub()
323
+ }
324
+ })
325
+
326
+ const command = new EPUBCommand()
327
+ command.name = sandbox.stub().returns('epub')
328
+
329
+ // Error is propagated; reporter lifecycle is handled by façade (lib/epub/index.js)
330
+ await t.throwsAsync(() => command.action({ engine: 'epubjs' }, command), { message: 'EPUB generation failed' })
331
+ })
332
+
333
+ test('epub command should configure reporter with quiet option', async (t) => {
334
+ const { sandbox, mockReporter } = t.context
335
+
336
+ const mockGenerateEpub = sandbox.stub().resolves('/project/epubjs.epub')
337
+
338
+ const EPUBCommand = await esmock('./epub.js', {
339
+ '#lib/epub/index.js': {
340
+ default: mockGenerateEpub
341
+ },
342
+ '#lib/project/index.js': {
343
+ hasEpubOutput: () => true
344
+ },
345
+ '#lib/reporter/index.js': {
346
+ default: mockReporter
347
+ },
348
+ 'fs-extra': {
349
+ existsSync: () => true
350
+ },
351
+ open: {
352
+ default: sandbox.stub()
353
+ }
354
+ })
355
+
356
+ const command = new EPUBCommand()
357
+ command.name = sandbox.stub().returns('epub')
358
+
359
+ await command.action({ engine: 'epubjs', quiet: true }, command)
360
+
361
+ t.true(
362
+ mockReporter.configure.calledWith(sinon.match({ quiet: true })),
363
+ 'reporter.configure should be called with quiet option'
364
+ )
365
+ })
366
+
367
+ test('epub command should pass output option to generateEpub', async (t) => {
368
+ const { sandbox, mockReporter } = t.context
369
+
370
+ const customOutput = '/custom/path/my-book.epub'
371
+ const mockGenerateEpub = sandbox.stub().resolves(customOutput)
372
+
373
+ const EPUBCommand = await esmock('./epub.js', {
374
+ '#lib/epub/index.js': {
375
+ default: mockGenerateEpub
376
+ },
377
+ '#lib/project/index.js': {
378
+ hasEpubOutput: () => true
379
+ },
380
+ '#lib/reporter/index.js': {
381
+ default: mockReporter
382
+ },
383
+ 'fs-extra': {
384
+ existsSync: () => true
385
+ },
386
+ open: {
387
+ default: sandbox.stub()
388
+ }
389
+ })
390
+
391
+ const command = new EPUBCommand()
392
+ command.name = sandbox.stub().returns('epub')
393
+
394
+ await command.action({ engine: 'epubjs', output: customOutput }, command)
395
+
396
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
397
+ t.is(mockGenerateEpub.firstCall.args[0].output, customOutput, 'should pass output option to generateEpub')
398
+ })
399
+
400
+ test('epub command should use epubEngine from config when --engine not specified', async (t) => {
401
+ const { sandbox, mockReporter } = t.context
402
+
403
+ const mockGenerateEpub = sandbox.stub().resolves('/project/pandoc.epub')
404
+
405
+ const EPUBCommand = await esmock('./epub.js', {
406
+ '#lib/epub/index.js': {
407
+ default: mockGenerateEpub
408
+ },
409
+ '#lib/project/index.js': {
410
+ hasEpubOutput: () => true
411
+ },
412
+ '#lib/reporter/index.js': {
413
+ default: mockReporter
414
+ },
415
+ 'fs-extra': {
416
+ existsSync: () => true
417
+ },
418
+ open: {
419
+ default: sandbox.stub()
420
+ }
421
+ })
422
+
423
+ const command = new EPUBCommand()
424
+ command.name = sandbox.stub().returns('epub')
425
+ // Mock config to return 'pandoc' for epubEngine
426
+ command.config = { get: (key) => key === 'epubEngine' ? 'pandoc' : undefined }
427
+
428
+ // No --engine specified, should use config value
429
+ await command.action({}, command)
430
+
431
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
432
+ t.is(mockGenerateEpub.firstCall.args[0].lib, 'pandoc', 'should use epubEngine from config')
433
+ })
434
+
435
+ test('epub command should use default engine when --engine not specified and config not set', async (t) => {
436
+ const { sandbox, mockReporter } = t.context
437
+
438
+ const mockGenerateEpub = sandbox.stub().resolves('/project/epubjs.epub')
439
+
440
+ const EPUBCommand = await esmock('./epub.js', {
441
+ '#lib/epub/index.js': {
442
+ default: mockGenerateEpub
443
+ },
444
+ '#lib/project/index.js': {
445
+ hasEpubOutput: () => true
446
+ },
447
+ '#lib/reporter/index.js': {
448
+ default: mockReporter
449
+ },
450
+ 'fs-extra': {
451
+ existsSync: () => true
452
+ },
453
+ open: {
454
+ default: sandbox.stub()
455
+ }
456
+ })
457
+
458
+ const command = new EPUBCommand()
459
+ command.name = sandbox.stub().returns('epub')
460
+ // Mock config to return undefined (no epubEngine set)
461
+ command.config = { get: () => undefined }
462
+
463
+ // No --engine specified, no config, should use default 'epubjs'
464
+ await command.action({}, command)
465
+
466
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
467
+ t.is(mockGenerateEpub.firstCall.args[0].lib, 'epubjs', 'should use default epubjs when no config')
468
+ })
469
+
470
+ test('epub command --engine flag should override config epubEngine', async (t) => {
471
+ const { sandbox, mockReporter } = t.context
472
+
473
+ const mockGenerateEpub = sandbox.stub().resolves('/project/epubjs.epub')
474
+
475
+ const EPUBCommand = await esmock('./epub.js', {
476
+ '#lib/epub/index.js': {
477
+ default: mockGenerateEpub
478
+ },
479
+ '#lib/project/index.js': {
480
+ hasEpubOutput: () => true
481
+ },
482
+ '#lib/reporter/index.js': {
483
+ default: mockReporter
484
+ },
485
+ 'fs-extra': {
486
+ existsSync: () => true
487
+ },
488
+ open: {
489
+ default: sandbox.stub()
490
+ }
491
+ })
492
+
493
+ const command = new EPUBCommand()
494
+ command.name = sandbox.stub().returns('epub')
495
+ // Mock config to return 'pandoc' for epubEngine
496
+ command.config = { get: (key) => key === 'epubEngine' ? 'pandoc' : undefined }
497
+
498
+ // --engine epubjs should override config's 'pandoc'
499
+ await command.action({ engine: 'epubjs' }, command)
500
+
501
+ t.true(mockGenerateEpub.called, 'generateEpub should be called')
502
+ t.is(mockGenerateEpub.firstCall.args[0].lib, 'epubjs', 'CLI --engine should override config')
503
+ })
@@ -8,14 +8,21 @@ const __dirname = path.dirname(__filename)
8
8
 
9
9
  const thisfile = path.basename(__filename)
10
10
 
11
+ /**
12
+ * Filter out files that are not command modules
13
+ * @param directory entry
14
+ * @returns Boolean
15
+ */
16
+ const filterEntries = (entry) => entry !== thisfile && entry.match(/^.*(?<!spec|test)\.js$/)
17
+
11
18
  /**
12
19
  * Dynamically import command modules in parallel
13
20
  * and call the class constuctor for each command
14
21
  */
15
22
  const commands = await Promise.all(
16
23
  fs.readdirSync(__dirname)
17
- .filter((file) => file !== thisfile && file.match(/^.*\.js$/))
18
- .map(async (file) => await dynamicImport(path.resolve(__dirname, file)))
24
+ .filter((entry) => filterEntries(entry))
25
+ .map(async (entry) => await dynamicImport(path.resolve(__dirname, entry)))
19
26
  ).then((modules) => {
20
27
  return modules.map(({ default: CommandClass }) => new CommandClass())
21
28
  })
@@ -1,5 +1,6 @@
1
1
  import Command from '#src/Command.js'
2
- import { execaCommand } from 'execa'
2
+ import npm from '#lib/npm/index.js'
3
+ import { execa } from 'execa'
3
4
  import fs from 'node:fs'
4
5
  import os from 'node:os'
5
6
  import path from 'node:path'
@@ -17,9 +18,13 @@ export default class InfoCommand extends Command {
17
18
  static definition = {
18
19
  name: 'info',
19
20
  description: 'List Quire cli, quire-11ty, and node versions',
20
- summary: 'list info',
21
+ summary: 'show version information',
22
+ docsLink: 'quire-commands/#get-help',
23
+ helpText: `
24
+ Example:
25
+ quire info --debug Include node, npm, and OS versions
26
+ `,
21
27
  version: '1.0.0',
22
- args: [],
23
28
  options: [
24
29
  ['--debug', 'include os versions in output']
25
30
  ],
@@ -30,15 +35,7 @@ export default class InfoCommand extends Command {
30
35
  }
31
36
 
32
37
  async action(options, command) {
33
- if (options.debug) {
34
- console.debug(
35
- '[CLI] Command \'%s\' called with options %o',
36
- this.name(),
37
- options
38
- )
39
- } else {
40
- console.debug('[CLI] Command \'%s\' called', this.name())
41
- }
38
+ this.debug('called with options %O', options)
42
39
 
43
40
  // Load filename from config with a default constraint if it doesn't exist
44
41
  const versionFileName = this.config.get('versionFile')
@@ -49,7 +46,7 @@ export default class InfoCommand extends Command {
49
46
 
50
47
  versionInfo = JSON.parse(versionFileData)
51
48
  } catch (error) {
52
- console.warn(
49
+ this.logger.warn(
53
50
  `This project was generated with the quire-cli prior to version 1.0.0.rc-8. Updating the version file to the new format, though this project's version file will not contain specific starter version information.`
54
51
  )
55
52
 
@@ -85,7 +82,7 @@ export default class InfoCommand extends Command {
85
82
  {
86
83
  name: 'quire-cli',
87
84
  get: async () => {
88
- const { stdout } = await execaCommand('quire --version')
85
+ const { stdout } = await execa('quire', ['--version'])
89
86
  return stdout
90
87
  },
91
88
  },
@@ -97,10 +94,7 @@ export default class InfoCommand extends Command {
97
94
  {
98
95
  debug: true,
99
96
  name: 'npm',
100
- get: async () => {
101
- const { stdout } = await execaCommand('npm --version')
102
- return stdout
103
- },
97
+ get: async () => await npm.version(),
104
98
  },
105
99
  {
106
100
  debug: true,
@@ -114,17 +108,17 @@ export default class InfoCommand extends Command {
114
108
  /**
115
109
  * Filter the command output based on `debug` settings
116
110
  */
117
- versions.forEach(async ({ items, title }) => {
118
- const versions = await Promise.all(
111
+ for (const { items, title } of versions) {
112
+ const versionList = await Promise.all(
119
113
  items
120
114
  .filter(({ debug }) => !debug || (options.debug && debug))
121
115
  .map(async ({ name, get }) => `${name} ${await get()}`)
122
116
  )
123
- console.info(`${title}\n ${versions.join('\n ')}`)
124
- })
117
+ this.logger.info(`${title}\n ${versionList.join('\n ')}`)
118
+ }
125
119
  }
126
120
 
127
- preAction(command) {
128
- testcwd(command)
121
+ preAction(thisCommand, actionCommand) {
122
+ testcwd(thisCommand)
129
123
  }
130
124
  }
@@ -0,0 +1,64 @@
1
+ import { Command, Option } from 'commander'
2
+ import program from '#src/main.js'
3
+ import test from 'ava'
4
+
5
+ /**
6
+ * Command Contract/Interface Tests
7
+ *
8
+ * Verifies the command's public API and Commander.js integration.
9
+ * @see docs/testing-commands.md
10
+ */
11
+
12
+ test.before((t) => {
13
+ // Get the registered command (from program.commands) once and share across all tests
14
+ t.context.command = program.commands.find((cmd) => cmd.name() === 'info')
15
+ })
16
+
17
+ test('command is registered in CLI program', (t) => {
18
+ const { command } = t.context
19
+
20
+ t.truthy(command, 'command "info" should be registered in program')
21
+ t.true(command instanceof Command, 'registered command should be Commander.js Command instance')
22
+ })
23
+
24
+ test('registered command has correct metadata', (t) => {
25
+ const { command } = t.context
26
+
27
+ t.is(command.name(), 'info')
28
+ t.truthy(command.description())
29
+ t.is(typeof command._actionHandler, 'function', 'command should have action handler')
30
+ })
31
+
32
+ test('registered command has no arguments', (t) => {
33
+ const { command } = t.context
34
+ const registeredArguments = command.registeredArguments
35
+
36
+ t.is(registeredArguments.length, 0, 'info command should have no arguments')
37
+ })
38
+
39
+ test('registered command has correct options', (t) => {
40
+ const { command } = t.context
41
+
42
+ // Get all options
43
+ const debugOption = command.options.find((opt) => opt.long === '--debug')
44
+
45
+ // Verify all options exist
46
+ t.truthy(debugOption, '--debug option should exist')
47
+
48
+ // Verify they are Option instances
49
+ t.true(debugOption instanceof Option, '--debug should be Option instance')
50
+
51
+ // Verify option properties
52
+ t.is(debugOption.long, '--debug')
53
+ t.truthy(debugOption.description)
54
+ t.false(debugOption.required, '--debug should not require a value')
55
+ })
56
+
57
+ test('command options are accessible via public API', (t) => {
58
+ const { command } = t.context
59
+
60
+ // Test that options can be accessed the way Commander.js does
61
+ const optionNames = command.options.map((opt) => opt.long)
62
+
63
+ t.true(optionNames.includes('--debug'))
64
+ })