@thegetty/quire-cli 1.0.0-rc.35 → 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 (158) 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/patches/README.md +19 -0
  6. package/patches/install-npm-version+1.0.9.patch +12119 -0
  7. package/schemas/objects.schema.json +12 -38
  8. package/schemas/publication.schema.json +0 -22
  9. package/schemas/references.schema.json +0 -1
  10. package/src/Command.js +26 -6
  11. package/src/Command.spec.js +99 -0
  12. package/src/commands/README.md +213 -122
  13. package/src/commands/build.js +46 -37
  14. package/src/commands/build.spec.js +113 -0
  15. package/src/commands/build.test.js +402 -0
  16. package/src/commands/clean.js +25 -19
  17. package/src/commands/clean.spec.js +108 -0
  18. package/src/commands/clean.test.js +260 -0
  19. package/src/commands/config.js +251 -0
  20. package/src/commands/config.spec.js +107 -0
  21. package/src/commands/config.test.js +715 -0
  22. package/src/commands/create.js +42 -14
  23. package/src/commands/create.spec.js +112 -0
  24. package/src/commands/create.test.js +415 -0
  25. package/src/commands/epub.js +59 -30
  26. package/src/commands/epub.spec.js +114 -0
  27. package/src/commands/epub.test.js +503 -0
  28. package/src/commands/index.js +9 -2
  29. package/src/commands/info.js +18 -24
  30. package/src/commands/info.spec.js +64 -0
  31. package/src/commands/info.test.js +415 -0
  32. package/src/commands/pdf.js +58 -84
  33. package/src/commands/pdf.spec.js +114 -0
  34. package/src/commands/pdf.test.js +464 -0
  35. package/src/commands/preview.js +26 -31
  36. package/src/commands/preview.spec.js +97 -0
  37. package/src/commands/preview.test.js +250 -0
  38. package/src/commands/use.js +56 -0
  39. package/src/commands/use.spec.js +61 -0
  40. package/src/commands/use.test.js +280 -0
  41. package/src/commands/validate.js +31 -19
  42. package/src/commands/validate.spec.js +82 -0
  43. package/src/commands/validate.test.js +234 -0
  44. package/src/commands/workflows.js +70 -0
  45. package/src/errors/build/build-failed-error.js +19 -0
  46. package/src/errors/build/config-field-missing-error.js +20 -0
  47. package/src/errors/build/config-file-not-found-error.js +20 -0
  48. package/src/errors/build/index.js +11 -0
  49. package/src/errors/index.js +48 -0
  50. package/src/errors/install/dependency-install-error.js +19 -0
  51. package/src/errors/install/directory-not-empty-error.js +25 -0
  52. package/src/errors/install/index.js +12 -0
  53. package/src/errors/install/invalid-path-error.js +31 -0
  54. package/src/errors/install/invalid-starter-error.js +27 -0
  55. package/src/errors/install/version-not-found-error.js +35 -0
  56. package/src/errors/output/epub-generation-error.js +19 -0
  57. package/src/errors/output/index.js +14 -0
  58. package/src/errors/output/invalid-epub-library-error.js +20 -0
  59. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  60. package/src/errors/output/missing-build-output-error.js +21 -0
  61. package/src/errors/output/pdf-generation-error.js +24 -0
  62. package/src/errors/output/tool-not-found-error.js +37 -0
  63. package/src/errors/project/index.js +10 -0
  64. package/src/errors/project/not-in-project-error.js +20 -0
  65. package/src/errors/project/project-create-error.js +21 -0
  66. package/src/errors/quire-error.js +27 -0
  67. package/src/errors/validation/validation-error.js +20 -11
  68. package/src/helpers/clean.js +1 -1
  69. package/src/helpers/docs-url.js +32 -0
  70. package/src/helpers/test-cwd.js +5 -6
  71. package/src/helpers/test-cwd.test.js +192 -0
  72. package/src/helpers/which.js +10 -4
  73. package/src/lib/11ty/README.md +135 -19
  74. package/src/lib/11ty/api.js +176 -93
  75. package/src/lib/11ty/cli.js +77 -35
  76. package/src/lib/11ty/index.js +64 -5
  77. package/src/lib/11ty/index.test.js +655 -0
  78. package/src/lib/README.md +275 -0
  79. package/src/lib/commander/index.js +100 -0
  80. package/src/lib/commander/index.test.js +86 -0
  81. package/src/lib/commander/options.js +195 -0
  82. package/src/lib/commander/options.test.js +109 -0
  83. package/src/lib/conf/README.md +84 -73
  84. package/src/lib/conf/config.js +5 -3
  85. package/src/lib/conf/config.test.js +281 -0
  86. package/src/lib/conf/defaults.js +44 -0
  87. package/src/lib/conf/format.js +60 -0
  88. package/src/lib/conf/format.test.js +106 -0
  89. package/src/lib/conf/helpers.js +91 -0
  90. package/src/lib/conf/helpers.test.js +136 -0
  91. package/src/lib/conf/index.js +22 -0
  92. package/src/lib/conf/schema.js +53 -8
  93. package/src/lib/epub/README.md +133 -2
  94. package/src/lib/epub/engines.js +46 -0
  95. package/src/lib/epub/epub.js +36 -11
  96. package/src/lib/epub/index.js +111 -21
  97. package/src/lib/epub/index.test.js +518 -0
  98. package/src/lib/epub/pandoc.js +33 -4
  99. package/src/lib/epub/pandoc.test.js +122 -0
  100. package/src/lib/epub/schema.js +21 -0
  101. package/src/lib/error/README.md +170 -0
  102. package/src/lib/error/handler.js +107 -0
  103. package/src/lib/git/README.md +151 -2
  104. package/src/lib/git/index.js +217 -13
  105. package/src/lib/git/index.spec.js +80 -0
  106. package/src/lib/git/index.test.js +453 -0
  107. package/src/lib/installer/index.js +309 -0
  108. package/src/lib/installer/index.spec.js +83 -0
  109. package/src/lib/installer/index.test.js +545 -0
  110. package/src/lib/logger/README.md +424 -0
  111. package/src/lib/logger/debug.js +90 -0
  112. package/src/lib/logger/debug.spec.js +130 -0
  113. package/src/lib/logger/index.js +228 -0
  114. package/src/lib/logger/index.spec.js +131 -0
  115. package/src/lib/logger/index.test.js +477 -0
  116. package/src/lib/npm/README.md +127 -0
  117. package/src/lib/npm/index.js +198 -0
  118. package/src/lib/npm/index.spec.js +60 -0
  119. package/src/lib/npm/index.test.js +355 -0
  120. package/src/lib/pdf/README.md +131 -0
  121. package/src/lib/pdf/engines.js +46 -0
  122. package/src/lib/pdf/index.js +124 -21
  123. package/src/lib/pdf/index.test.js +708 -0
  124. package/src/lib/pdf/paged.js +100 -52
  125. package/src/lib/pdf/paged.test.js +366 -0
  126. package/src/lib/pdf/prince.js +115 -37
  127. package/src/lib/pdf/prince.test.js +202 -0
  128. package/src/lib/pdf/schema.js +21 -0
  129. package/src/lib/pdf/split.js +61 -33
  130. package/src/lib/pdf/split.test.js +445 -0
  131. package/src/lib/process/manager.js +110 -0
  132. package/src/lib/process/manager.test.js +55 -0
  133. package/src/lib/project/build.js +143 -0
  134. package/src/lib/project/build.test.js +253 -0
  135. package/src/lib/project/config.js +48 -0
  136. package/src/lib/project/config.test.js +134 -0
  137. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  138. package/src/lib/project/detect.test.js +157 -0
  139. package/src/lib/project/index.js +40 -0
  140. package/src/lib/project/paths.js +224 -0
  141. package/src/lib/project/version.js +110 -0
  142. package/src/lib/project/version.test.js +350 -0
  143. package/src/lib/reporter/README.md +211 -2
  144. package/src/lib/reporter/index.js +512 -0
  145. package/src/lib/reporter/index.test.js +593 -0
  146. package/src/main.js +134 -47
  147. package/src/main.spec.js +61 -0
  148. package/src/main.test.js +347 -0
  149. package/src/validators/utils.js +2 -1
  150. package/src/commands/conf.js +0 -43
  151. package/src/commands/version.js +0 -43
  152. package/src/lib/11ty/paths.js +0 -103
  153. package/src/lib/i18n/README.md +0 -3
  154. package/src/lib/i18n/config.js +0 -53
  155. package/src/lib/i18n/index.js +0 -43
  156. package/src/lib/i18n/localeService.js +0 -58
  157. package/src/lib/quire/README.md +0 -19
  158. package/src/lib/quire/index.js +0 -350
@@ -0,0 +1,715 @@
1
+ import test from 'ava'
2
+ import { Volume, createFsFromVolume } from 'memfs'
3
+ import sinon from 'sinon'
4
+ import esmock from 'esmock'
5
+
6
+ test.beforeEach((t) => {
7
+ // Create sinon sandbox for mocking
8
+ t.context.sandbox = sinon.createSandbox()
9
+
10
+ // Create in-memory file system
11
+ t.context.vol = new Volume()
12
+ t.context.fs = createFsFromVolume(t.context.vol)
13
+
14
+ // Create mock logger (no global console stubbing needed!)
15
+ t.context.mockLogger = {
16
+ info: t.context.sandbox.stub(),
17
+ error: t.context.sandbox.stub(),
18
+ debug: t.context.sandbox.stub(),
19
+ log: t.context.sandbox.stub(),
20
+ warn: t.context.sandbox.stub()
21
+ }
22
+
23
+ // Mock config store
24
+ t.context.mockConfig = {
25
+ path: '/mock/config/path',
26
+ store: {
27
+ logLevel: 'info',
28
+ logShowLevel: false,
29
+ projectTemplate: 'https://github.com/thegetty/quire-starter-default',
30
+ quireVersion: '1.0.0',
31
+ __internal__secretKey: 'hidden-value'
32
+ },
33
+ get: (key) => t.context.mockConfig.store[key],
34
+ set: t.context.sandbox.stub(),
35
+ delete: t.context.sandbox.stub(),
36
+ reset: t.context.sandbox.stub(),
37
+ clear: t.context.sandbox.stub()
38
+ }
39
+ })
40
+
41
+ test.afterEach.always((t) => {
42
+ // Restore all mocks
43
+ t.context.sandbox.restore()
44
+
45
+ // Clear in-memory file system
46
+ t.context.vol.reset()
47
+ })
48
+
49
+ // =============================================================================
50
+ // Show all (no operation)
51
+ // =============================================================================
52
+
53
+ test('conf command should display all config when no operation provided', async (t) => {
54
+ const { sandbox, mockLogger, mockConfig } = t.context
55
+
56
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
57
+ '#lib/logger/index.js': {
58
+ default: () => mockLogger
59
+ }
60
+ })
61
+
62
+ const command = new ConfCommand()
63
+ command.config = mockConfig
64
+ command.logger = mockLogger
65
+ command.debug = sandbox.stub()
66
+
67
+ await command.action(undefined, undefined, undefined, {})
68
+
69
+ // Should be called once with all output joined
70
+ t.true(mockLogger.info.calledOnce)
71
+
72
+ const output = mockLogger.info.firstCall.args[0]
73
+ t.true(output.includes('quire-cli configuration /mock/config/path'))
74
+ t.true(output.includes('logLevel: "info"'))
75
+ t.true(output.includes('quireVersion: "1.0.0"'))
76
+ })
77
+
78
+ test('conf command should hide internal config values by default', async (t) => {
79
+ const { sandbox, mockLogger, mockConfig } = t.context
80
+
81
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
82
+ '#lib/logger/index.js': {
83
+ default: () => mockLogger
84
+ }
85
+ })
86
+
87
+ const command = new ConfCommand()
88
+ command.config = mockConfig
89
+ command.logger = mockLogger
90
+ command.debug = sandbox.stub()
91
+
92
+ await command.action(undefined, undefined, undefined, {})
93
+
94
+ const output = mockLogger.info.firstCall.args[0]
95
+ t.false(output.includes('__internal__secretKey'))
96
+ })
97
+
98
+ test('conf command should show internal config values with debug flag', async (t) => {
99
+ const { sandbox, mockLogger, mockConfig } = t.context
100
+
101
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
102
+ '#lib/logger/index.js': {
103
+ default: () => mockLogger
104
+ }
105
+ })
106
+
107
+ const command = new ConfCommand()
108
+ command.config = mockConfig
109
+ command.logger = mockLogger
110
+ command.debug = sandbox.stub()
111
+
112
+ await command.action(undefined, undefined, undefined, { debug: true })
113
+
114
+ const output = mockLogger.info.firstCall.args[0]
115
+ t.true(output.includes('__internal__secretKey: "hidden-value"'))
116
+ })
117
+
118
+ // =============================================================================
119
+ // Get operation
120
+ // =============================================================================
121
+
122
+ test('conf get should return a single value', async (t) => {
123
+ const { sandbox, mockLogger, mockConfig } = t.context
124
+
125
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
126
+ '#lib/logger/index.js': {
127
+ default: () => mockLogger
128
+ }
129
+ })
130
+
131
+ const command = new ConfCommand()
132
+ command.config = mockConfig
133
+ command.logger = mockLogger
134
+ command.debug = sandbox.stub()
135
+
136
+ await command.action('get', 'logLevel', undefined, {})
137
+
138
+ t.true(mockLogger.info.calledOnce)
139
+ t.true(mockLogger.info.calledWith('logLevel: "info"'))
140
+ })
141
+
142
+ test('conf get should show error when key is missing', async (t) => {
143
+ const { sandbox, mockLogger, mockConfig } = t.context
144
+
145
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
146
+ '#lib/logger/index.js': {
147
+ default: () => mockLogger
148
+ }
149
+ })
150
+
151
+ const command = new ConfCommand()
152
+ command.config = mockConfig
153
+ command.logger = mockLogger
154
+ command.debug = sandbox.stub()
155
+
156
+ await command.action('get', undefined, undefined, {})
157
+
158
+ t.true(mockLogger.error.calledOnce)
159
+ t.true(mockLogger.error.calledWith('Usage: quire conf get <key>'))
160
+ })
161
+
162
+ test('conf get should show error for unknown key', async (t) => {
163
+ const { sandbox, mockLogger, mockConfig } = t.context
164
+
165
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
166
+ '#lib/logger/index.js': {
167
+ default: () => mockLogger
168
+ }
169
+ })
170
+
171
+ const command = new ConfCommand()
172
+ command.config = mockConfig
173
+ command.logger = mockLogger
174
+ command.debug = sandbox.stub()
175
+
176
+ await command.action('get', 'unknownKey', undefined, {})
177
+
178
+ t.true(mockLogger.error.calledOnce)
179
+ t.true(mockLogger.error.calledWith('Unknown configuration key: unknownKey'))
180
+ t.true(mockLogger.info.calledOnce)
181
+ t.true(mockLogger.info.firstCall.args[0].includes('Valid keys:'))
182
+ })
183
+
184
+ // =============================================================================
185
+ // Set operation
186
+ // =============================================================================
187
+
188
+ test('conf set should set a value', async (t) => {
189
+ const { sandbox, mockLogger, mockConfig } = t.context
190
+
191
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
192
+ '#lib/logger/index.js': {
193
+ default: () => mockLogger
194
+ }
195
+ })
196
+
197
+ const command = new ConfCommand()
198
+ command.config = mockConfig
199
+ command.logger = mockLogger
200
+ command.debug = sandbox.stub()
201
+
202
+ await command.action('set', 'logLevel', 'debug', {})
203
+
204
+ t.true(mockConfig.set.calledOnce)
205
+ t.true(mockConfig.set.calledWith('logLevel', 'debug'))
206
+ t.true(mockLogger.info.calledWith('logLevel: "debug"'))
207
+ })
208
+
209
+ test('conf set should show error when key is missing', async (t) => {
210
+ const { sandbox, mockLogger, mockConfig } = t.context
211
+
212
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
213
+ '#lib/logger/index.js': {
214
+ default: () => mockLogger
215
+ }
216
+ })
217
+
218
+ const command = new ConfCommand()
219
+ command.config = mockConfig
220
+ command.logger = mockLogger
221
+ command.debug = sandbox.stub()
222
+
223
+ await command.action('set', undefined, undefined, {})
224
+
225
+ t.true(mockLogger.error.calledOnce)
226
+ t.true(mockLogger.error.calledWith('Usage: quire conf set <key> <value>'))
227
+ })
228
+
229
+ test('conf set should show error when value is missing', async (t) => {
230
+ const { sandbox, mockLogger, mockConfig } = t.context
231
+
232
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
233
+ '#lib/logger/index.js': {
234
+ default: () => mockLogger
235
+ }
236
+ })
237
+
238
+ const command = new ConfCommand()
239
+ command.config = mockConfig
240
+ command.logger = mockLogger
241
+ command.debug = sandbox.stub()
242
+
243
+ await command.action('set', 'logLevel', undefined, {})
244
+
245
+ t.true(mockLogger.error.calledOnce)
246
+ t.true(mockLogger.error.calledWith('Usage: quire conf set <key> <value>'))
247
+ })
248
+
249
+ test('conf set should coerce boolean true values', async (t) => {
250
+ const { sandbox, mockLogger, mockConfig } = t.context
251
+
252
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
253
+ '#lib/logger/index.js': {
254
+ default: () => mockLogger
255
+ }
256
+ })
257
+
258
+ const command = new ConfCommand()
259
+ command.config = mockConfig
260
+ command.logger = mockLogger
261
+ command.debug = sandbox.stub()
262
+
263
+ await command.action('set', 'logShowLevel', 'true', {})
264
+
265
+ t.true(mockConfig.set.calledWith('logShowLevel', true))
266
+ })
267
+
268
+ test('conf set should coerce boolean false values', async (t) => {
269
+ const { sandbox, mockLogger, mockConfig } = t.context
270
+
271
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
272
+ '#lib/logger/index.js': {
273
+ default: () => mockLogger
274
+ }
275
+ })
276
+
277
+ const command = new ConfCommand()
278
+ command.config = mockConfig
279
+ command.logger = mockLogger
280
+ command.debug = sandbox.stub()
281
+
282
+ await command.action('set', 'logShowLevel', 'false', {})
283
+
284
+ t.true(mockConfig.set.calledWith('logShowLevel', false))
285
+ })
286
+
287
+ test('conf set should coerce numeric boolean values', async (t) => {
288
+ const { sandbox, mockLogger, mockConfig } = t.context
289
+
290
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
291
+ '#lib/logger/index.js': {
292
+ default: () => mockLogger
293
+ }
294
+ })
295
+
296
+ const command = new ConfCommand()
297
+ command.config = mockConfig
298
+ command.logger = mockLogger
299
+ command.debug = sandbox.stub()
300
+
301
+ await command.action('set', 'logShowLevel', '1', {})
302
+ t.true(mockConfig.set.calledWith('logShowLevel', true))
303
+
304
+ mockConfig.set.resetHistory()
305
+
306
+ await command.action('set', 'logShowLevel', '0', {})
307
+ t.true(mockConfig.set.calledWith('logShowLevel', false))
308
+ })
309
+
310
+ test('conf set should show validation error for invalid enum value', async (t) => {
311
+ const { sandbox, mockLogger, mockConfig } = t.context
312
+
313
+ // Make config.set throw an error for invalid values
314
+ mockConfig.set.throws(new Error('Schema validation failed'))
315
+
316
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
317
+ '#lib/logger/index.js': {
318
+ default: () => mockLogger
319
+ }
320
+ })
321
+
322
+ const command = new ConfCommand()
323
+ command.config = mockConfig
324
+ command.logger = mockLogger
325
+ command.debug = sandbox.stub()
326
+
327
+ await command.action('set', 'logLevel', 'invalid', {})
328
+
329
+ t.true(mockLogger.error.calledOnce)
330
+ const errorOutput = mockLogger.error.firstCall.args[0]
331
+ t.true(errorOutput.includes('Invalid value'))
332
+ t.true(errorOutput.includes('Valid values:'))
333
+ })
334
+
335
+ // =============================================================================
336
+ // Delete operation
337
+ // =============================================================================
338
+
339
+ test('conf delete should delete a key', async (t) => {
340
+ const { sandbox, mockLogger, mockConfig } = t.context
341
+
342
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
343
+ '#lib/logger/index.js': {
344
+ default: () => mockLogger
345
+ }
346
+ })
347
+
348
+ const command = new ConfCommand()
349
+ command.config = mockConfig
350
+ command.logger = mockLogger
351
+ command.debug = sandbox.stub()
352
+
353
+ await command.action('delete', 'logLevel', undefined, {})
354
+
355
+ t.true(mockConfig.delete.calledOnce)
356
+ t.true(mockConfig.delete.calledWith('logLevel'))
357
+ t.true(mockLogger.info.calledOnce)
358
+ t.true(mockLogger.info.firstCall.args[0].includes('reset to'))
359
+ })
360
+
361
+ test('conf delete should show error when key is missing', async (t) => {
362
+ const { sandbox, mockLogger, mockConfig } = t.context
363
+
364
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
365
+ '#lib/logger/index.js': {
366
+ default: () => mockLogger
367
+ }
368
+ })
369
+
370
+ const command = new ConfCommand()
371
+ command.config = mockConfig
372
+ command.logger = mockLogger
373
+ command.debug = sandbox.stub()
374
+
375
+ await command.action('delete', undefined, undefined, {})
376
+
377
+ t.true(mockLogger.error.calledOnce)
378
+ t.true(mockLogger.error.calledWith('Usage: quire conf delete <key>'))
379
+ t.false(mockConfig.delete.called)
380
+ })
381
+
382
+ test('conf delete should show error for unknown key', async (t) => {
383
+ const { sandbox, mockLogger, mockConfig } = t.context
384
+
385
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
386
+ '#lib/logger/index.js': {
387
+ default: () => mockLogger
388
+ }
389
+ })
390
+
391
+ const command = new ConfCommand()
392
+ command.config = mockConfig
393
+ command.logger = mockLogger
394
+ command.debug = sandbox.stub()
395
+
396
+ await command.action('delete', 'unknownKey', undefined, {})
397
+
398
+ t.true(mockLogger.error.calledOnce)
399
+ t.false(mockConfig.delete.called)
400
+ })
401
+
402
+ // =============================================================================
403
+ // Reset operation
404
+ // =============================================================================
405
+
406
+ test('conf reset should reset a single key', async (t) => {
407
+ const { sandbox, mockLogger, mockConfig } = t.context
408
+
409
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
410
+ '#lib/logger/index.js': {
411
+ default: () => mockLogger
412
+ }
413
+ })
414
+
415
+ const command = new ConfCommand()
416
+ command.config = mockConfig
417
+ command.logger = mockLogger
418
+ command.debug = sandbox.stub()
419
+
420
+ await command.action('reset', 'logLevel', undefined, {})
421
+
422
+ t.true(mockConfig.reset.calledOnce)
423
+ t.true(mockConfig.reset.calledWith('logLevel'))
424
+ t.true(mockLogger.info.firstCall.args[0].includes('reset to'))
425
+ })
426
+
427
+ test('conf reset should reset all config when no key provided', async (t) => {
428
+ const { sandbox, mockLogger, mockConfig } = t.context
429
+
430
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
431
+ '#lib/logger/index.js': {
432
+ default: () => mockLogger
433
+ }
434
+ })
435
+
436
+ const command = new ConfCommand()
437
+ command.config = mockConfig
438
+ command.logger = mockLogger
439
+ command.debug = sandbox.stub()
440
+
441
+ await command.action('reset', undefined, undefined, {})
442
+
443
+ t.true(mockConfig.clear.calledOnce)
444
+ t.true(mockLogger.info.calledWith('Configuration reset to defaults'))
445
+ })
446
+
447
+ test('conf reset should show error for unknown key', async (t) => {
448
+ const { sandbox, mockLogger, mockConfig } = t.context
449
+
450
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
451
+ '#lib/logger/index.js': {
452
+ default: () => mockLogger
453
+ }
454
+ })
455
+
456
+ const command = new ConfCommand()
457
+ command.config = mockConfig
458
+ command.logger = mockLogger
459
+ command.debug = sandbox.stub()
460
+
461
+ await command.action('reset', 'unknownKey', undefined, {})
462
+
463
+ t.true(mockLogger.error.calledOnce)
464
+ t.false(mockConfig.reset.called)
465
+ })
466
+
467
+ // =============================================================================
468
+ // Path operation
469
+ // =============================================================================
470
+
471
+ test('conf path should show config file path', async (t) => {
472
+ const { sandbox, mockLogger, mockConfig } = t.context
473
+
474
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
475
+ '#lib/logger/index.js': {
476
+ default: () => mockLogger
477
+ }
478
+ })
479
+
480
+ const command = new ConfCommand()
481
+ command.config = mockConfig
482
+ command.logger = mockLogger
483
+ command.debug = sandbox.stub()
484
+
485
+ await command.action('path', undefined, undefined, {})
486
+
487
+ t.true(mockLogger.info.calledOnce)
488
+ t.true(mockLogger.info.calledWith('/mock/config/path'))
489
+ })
490
+
491
+ // =============================================================================
492
+ // Invalid operation
493
+ // =============================================================================
494
+
495
+ test('conf should show error for unknown operation', async (t) => {
496
+ const { sandbox, mockLogger, mockConfig } = t.context
497
+
498
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
499
+ '#lib/logger/index.js': {
500
+ default: () => mockLogger
501
+ }
502
+ })
503
+
504
+ const command = new ConfCommand()
505
+ command.config = mockConfig
506
+ command.logger = mockLogger
507
+ command.debug = sandbox.stub()
508
+
509
+ await command.action('unknownOp', undefined, undefined, {})
510
+
511
+ t.true(mockLogger.error.calledOnce)
512
+ t.true(mockLogger.error.calledWith('Unknown operation: unknownOp'))
513
+ t.true(mockLogger.info.calledOnce)
514
+ t.true(mockLogger.info.firstCall.args[0].includes('Valid operations:'))
515
+ })
516
+
517
+ // =============================================================================
518
+ // JSON output
519
+ // =============================================================================
520
+
521
+ test.serial('conf --json should output raw JSON of all config', async (t) => {
522
+ const { sandbox, mockLogger, mockConfig } = t.context
523
+
524
+ const consoleLogStub = sandbox.stub(console, 'log')
525
+
526
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
527
+ '#lib/logger/index.js': {
528
+ default: () => mockLogger
529
+ }
530
+ })
531
+
532
+ const command = new ConfCommand()
533
+ command.config = mockConfig
534
+ command.logger = mockLogger
535
+ command.debug = sandbox.stub()
536
+
537
+ await command.action(undefined, undefined, undefined, { json: true })
538
+
539
+ t.true(consoleLogStub.calledOnce)
540
+ const output = JSON.parse(consoleLogStub.firstCall.args[0])
541
+ t.is(output.logLevel, 'info')
542
+ t.is(output.quireVersion, '1.0.0')
543
+ t.false(mockLogger.info.called, 'logger.info should not be called in JSON mode')
544
+ })
545
+
546
+ test.serial('conf --json should not include descriptions or headers', async (t) => {
547
+ const { sandbox, mockLogger, mockConfig } = t.context
548
+
549
+ const consoleLogStub = sandbox.stub(console, 'log')
550
+
551
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
552
+ '#lib/logger/index.js': {
553
+ default: () => mockLogger
554
+ }
555
+ })
556
+
557
+ const command = new ConfCommand()
558
+ command.config = mockConfig
559
+ command.logger = mockLogger
560
+ command.debug = sandbox.stub()
561
+
562
+ await command.action(undefined, undefined, undefined, { json: true })
563
+
564
+ const raw = consoleLogStub.firstCall.args[0]
565
+ t.false(raw.includes('quire-cli configuration'), 'JSON output should not contain header text')
566
+ t.false(raw.includes('Use "quire settings'), 'JSON output should not contain help hint')
567
+ })
568
+
569
+ test.serial('conf --json should exclude __internal__ keys by default', async (t) => {
570
+ const { sandbox, mockLogger, mockConfig } = t.context
571
+
572
+ const consoleLogStub = sandbox.stub(console, 'log')
573
+
574
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
575
+ '#lib/logger/index.js': {
576
+ default: () => mockLogger
577
+ }
578
+ })
579
+
580
+ const command = new ConfCommand()
581
+ command.config = mockConfig
582
+ command.logger = mockLogger
583
+ command.debug = sandbox.stub()
584
+
585
+ await command.action(undefined, undefined, undefined, { json: true })
586
+
587
+ const output = JSON.parse(consoleLogStub.firstCall.args[0])
588
+ t.false('__internal__secretKey' in output, 'JSON output should not include __internal__ keys')
589
+ t.is(output.logLevel, 'info', 'non-internal keys should still be present')
590
+ })
591
+
592
+ test.serial('conf --json --debug should include __internal__ keys', async (t) => {
593
+ const { sandbox, mockLogger, mockConfig } = t.context
594
+
595
+ const consoleLogStub = sandbox.stub(console, 'log')
596
+
597
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
598
+ '#lib/logger/index.js': {
599
+ default: () => mockLogger
600
+ }
601
+ })
602
+
603
+ const command = new ConfCommand()
604
+ command.config = mockConfig
605
+ command.logger = mockLogger
606
+ command.debug = sandbox.stub()
607
+
608
+ await command.action(undefined, undefined, undefined, { json: true, debug: true })
609
+
610
+ const output = JSON.parse(consoleLogStub.firstCall.args[0])
611
+ t.is(output.__internal__secretKey, 'hidden-value', '__internal__ keys should be included with --debug')
612
+ t.is(output.logLevel, 'info')
613
+ })
614
+
615
+ test.serial('conf get --json should output single value as JSON', async (t) => {
616
+ const { sandbox, mockLogger, mockConfig } = t.context
617
+
618
+ const consoleLogStub = sandbox.stub(console, 'log')
619
+
620
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
621
+ '#lib/logger/index.js': {
622
+ default: () => mockLogger
623
+ }
624
+ })
625
+
626
+ const command = new ConfCommand()
627
+ command.config = mockConfig
628
+ command.logger = mockLogger
629
+ command.debug = sandbox.stub()
630
+
631
+ await command.action('get', 'logLevel', undefined, { json: true })
632
+
633
+ t.true(consoleLogStub.calledOnce)
634
+ t.is(consoleLogStub.firstCall.args[0], '"info"')
635
+ t.false(mockLogger.info.called, 'logger.info should not be called in JSON mode')
636
+ })
637
+
638
+ test.serial('conf get --json should output boolean value as JSON', async (t) => {
639
+ const { sandbox, mockLogger, mockConfig } = t.context
640
+
641
+ const consoleLogStub = sandbox.stub(console, 'log')
642
+
643
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
644
+ '#lib/logger/index.js': {
645
+ default: () => mockLogger
646
+ }
647
+ })
648
+
649
+ const command = new ConfCommand()
650
+ command.config = mockConfig
651
+ command.logger = mockLogger
652
+ command.debug = sandbox.stub()
653
+
654
+ await command.action('get', 'logShowLevel', undefined, { json: true })
655
+
656
+ t.true(consoleLogStub.calledOnce)
657
+ t.is(consoleLogStub.firstCall.args[0], 'false')
658
+ })
659
+
660
+ test.serial('conf get --json should output undefined as JSON for unset key', async (t) => {
661
+ const { sandbox, mockLogger, mockConfig } = t.context
662
+
663
+ const consoleLogStub = sandbox.stub(console, 'log')
664
+
665
+ // Add a key that returns undefined
666
+ mockConfig.store.missingKey = undefined
667
+ mockConfig.get = (key) => mockConfig.store[key]
668
+
669
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
670
+ '#lib/logger/index.js': {
671
+ default: () => mockLogger
672
+ }
673
+ })
674
+
675
+ const command = new ConfCommand()
676
+ command.config = mockConfig
677
+ command.logger = mockLogger
678
+ command.debug = sandbox.stub()
679
+
680
+ await command.action('get', 'logLevel', undefined, { json: true })
681
+
682
+ t.true(consoleLogStub.calledOnce)
683
+ })
684
+
685
+ // =============================================================================
686
+ // Debug output
687
+ // =============================================================================
688
+
689
+ test('conf command logs debug info with operation, key, value, and options', async (t) => {
690
+ const { sandbox, mockLogger, mockConfig } = t.context
691
+
692
+ const mockDebug = sandbox.stub()
693
+
694
+ const { default: ConfCommand } = await esmock('./config.js', {}, {
695
+ '#lib/logger/index.js': {
696
+ default: () => mockLogger
697
+ }
698
+ })
699
+
700
+ const command = new ConfCommand()
701
+ command.config = mockConfig
702
+ command.logger = mockLogger
703
+ command.debug = mockDebug
704
+
705
+ await command.action('set', 'logLevel', 'debug', { someOption: true })
706
+
707
+ t.true(mockDebug.called)
708
+ t.true(mockDebug.calledWith(
709
+ 'called with operation=%s key=%s value=%s options=%O',
710
+ 'set',
711
+ 'logLevel',
712
+ 'debug',
713
+ { someOption: true }
714
+ ))
715
+ })