@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,512 @@
1
+ /**
2
+ * Reporter module for CLI progress feedback
3
+ *
4
+ * Provides a consistent interface for progress indicators (spinners) with
5
+ * unified handling of quiet/verbose modes and testability. Follows the
6
+ * singleton pattern like lib/logger.js for easy mocking in tests.
7
+ *
8
+ * ## Output Modes
9
+ *
10
+ * The reporter supports three output modes controlled by command options:
11
+ *
12
+ * | Mode | Flag | Behavior |
13
+ * |------|------|----------|
14
+ * | Default | (none) | Show spinner with basic status |
15
+ * | Quiet | `-q, --quiet` | Suppress all output (for CI/scripts) |
16
+ * | Verbose | `-v, --verbose` | Show detailed progress with paths, timing |
17
+ *
18
+ * Note: `--debug` is handled separately and enables DEBUG namespace logging,
19
+ * not reporter output. Use verbose mode for detailed user-facing output.
20
+ *
21
+ * ## Config Defaults
22
+ *
23
+ * Users can set default values via `quire settings`:
24
+ * - `quire settings set verbose true` - Always run in verbose mode
25
+ *
26
+ * CLI flags override config settings. Use `--no-verbose` to disable verbose
27
+ * mode even when enabled in config.
28
+ *
29
+ * @example Production usage
30
+ * import reporter from '#lib/reporter/index.js'
31
+ * reporter.start('Building site...')
32
+ * await build()
33
+ * reporter.succeed('Build complete')
34
+ *
35
+ * @example Multi-phase operation
36
+ * reporter.start('Cloning starter...')
37
+ * await git.clone()
38
+ * reporter.update('Installing dependencies...')
39
+ * await npm.install()
40
+ * reporter.succeed('Project created')
41
+ *
42
+ * @example Error handling
43
+ * reporter.start('Generating PDF...')
44
+ * try {
45
+ * await generatePdf()
46
+ * reporter.succeed()
47
+ * } catch (error) {
48
+ * reporter.fail('PDF generation failed')
49
+ * throw error
50
+ * }
51
+ *
52
+ * @example Verbose mode with details
53
+ * reporter.configure({ verbose: true })
54
+ * reporter.start('Building site...')
55
+ * reporter.detail('Output: _site/')
56
+ * reporter.detail('Pages: 45')
57
+ * reporter.succeed('Build complete')
58
+ * // Shows:
59
+ * // ⠋ Building site...
60
+ * // Output: _site/
61
+ * // Pages: 45
62
+ * // ✔ Build complete
63
+ *
64
+ * @example Test mocking
65
+ * const mockReporter = {
66
+ * start: sandbox.stub(),
67
+ * succeed: sandbox.stub(),
68
+ * fail: sandbox.stub(),
69
+ * configure: sandbox.stub(),
70
+ * }
71
+ * const MyCommand = await esmock('./mycommand.js', {
72
+ * '#lib/reporter/index.js': { default: mockReporter }
73
+ * })
74
+ *
75
+ * @see https://github.com/sindresorhus/ora
76
+ * @module lib/reporter
77
+ */
78
+ import ora from 'ora'
79
+ import createDebug from '#debug'
80
+ import config from '#lib/conf/config.js'
81
+
82
+ const debug = createDebug('lib:reporter')
83
+
84
+ /**
85
+ * Reporter class for CLI progress feedback
86
+ *
87
+ * Wraps ora spinner with additional features:
88
+ * - Respects --quiet and --json flags
89
+ * - Supports elapsed time display
90
+ * - Provides consistent API across commands
91
+ */
92
+ class Reporter {
93
+ /** @type {import('ora').Ora|null} */
94
+ #spinner = null
95
+
96
+ /** @type {boolean} */
97
+ #quiet = false
98
+
99
+ /** @type {boolean} */
100
+ #json = false
101
+
102
+ /** @type {boolean} */
103
+ #verbose = false
104
+
105
+ /** @type {number|null} */
106
+ #startTime = null
107
+
108
+ /** @type {NodeJS.Timeout|null} */
109
+ #elapsedInterval = null
110
+
111
+ /** @type {string} */
112
+ #baseText = ''
113
+
114
+ /**
115
+ * Configure reporter for current command context
116
+ *
117
+ * Call this at the start of command action() to respect command options.
118
+ * CLI flags take precedence over config settings.
119
+ *
120
+ * @param {Object} options - Command options
121
+ * @param {boolean} [options.quiet] - Suppress all output (no config default)
122
+ * @param {boolean} [options.json] - JSON output mode (suppress spinner)
123
+ * @param {boolean} [options.verbose] - Verbose output mode (falls back to config.verbose)
124
+ * @returns {Reporter} this instance for chaining
125
+ *
126
+ * @example
127
+ * async action(options) {
128
+ * reporter.configure(options)
129
+ * reporter.start('Working...')
130
+ * }
131
+ */
132
+ configure(options = {}) {
133
+ this.#quiet = options.quiet || false
134
+ this.#json = options.json || false
135
+ // CLI flag takes precedence, then config setting, then default false
136
+ this.#verbose = options.verbose ?? config.get('verbose') ?? false
137
+ debug('configured: quiet=%s, json=%s, verbose=%s', this.#quiet, this.#json, this.#verbose)
138
+ return this
139
+ }
140
+
141
+ /**
142
+ * Check if output should be suppressed
143
+ * @private
144
+ * @returns {boolean}
145
+ */
146
+ #shouldSuppress() {
147
+ // Suppress if quiet mode, JSON mode, or not a TTY
148
+ return this.#quiet || this.#json || !process.stdout.isTTY
149
+ }
150
+
151
+ /**
152
+ * Start a new spinner
153
+ *
154
+ * @param {string} text - Initial spinner text
155
+ * @param {Object} [options] - Spinner options
156
+ * @param {boolean} [options.showElapsed=false] - Show elapsed time (e.g., "Building... (12s)")
157
+ * @returns {Reporter} this instance for chaining
158
+ *
159
+ * @example
160
+ * reporter.start('Building site...')
161
+ *
162
+ * @example With elapsed time
163
+ * reporter.start('Building site...', { showElapsed: true })
164
+ * // Shows: ⠋ Building site... (12s)
165
+ */
166
+ start(text, options = {}) {
167
+ // Stop any existing spinner
168
+ this.stop()
169
+
170
+ this.#baseText = text
171
+ this.#startTime = Date.now()
172
+
173
+ if (this.#shouldSuppress()) {
174
+ debug('spinner suppressed: %s', text)
175
+ return this
176
+ }
177
+
178
+ debug('spinner start: %s', text)
179
+ this.#spinner = ora({
180
+ text,
181
+ // Use 'dots' spinner for consistency across platforms
182
+ spinner: 'dots',
183
+ }).start()
184
+
185
+ // Start elapsed time updates if requested
186
+ if (options.showElapsed) {
187
+ this.#startElapsedTimer()
188
+ }
189
+
190
+ return this
191
+ }
192
+
193
+ /**
194
+ * Start interval timer to update elapsed time display
195
+ * @private
196
+ */
197
+ #startElapsedTimer() {
198
+ this.#elapsedInterval = setInterval(() => {
199
+ if (this.#spinner && this.#startTime) {
200
+ const elapsed = Math.floor((Date.now() - this.#startTime) / 1000)
201
+ this.#spinner.text = `${this.#baseText} (${elapsed}s)`
202
+ }
203
+ }, 1000)
204
+ }
205
+
206
+ /**
207
+ * Stop elapsed time timer
208
+ * @private
209
+ */
210
+ #stopElapsedTimer() {
211
+ if (this.#elapsedInterval) {
212
+ clearInterval(this.#elapsedInterval)
213
+ this.#elapsedInterval = null
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Update spinner text while running
219
+ *
220
+ * @param {string} text - New spinner text
221
+ * @returns {Reporter} this instance for chaining
222
+ *
223
+ * @example
224
+ * reporter.start('Step 1...')
225
+ * await step1()
226
+ * reporter.update('Step 2...')
227
+ * await step2()
228
+ * reporter.succeed('All steps complete')
229
+ */
230
+ update(text) {
231
+ this.#baseText = text
232
+
233
+ if (this.#shouldSuppress()) {
234
+ debug('spinner update (suppressed): %s', text)
235
+ return this
236
+ }
237
+
238
+ if (this.#spinner) {
239
+ debug('spinner update: %s', text)
240
+ this.#spinner.text = text
241
+ } else {
242
+ // If no spinner running, start one
243
+ this.start(text)
244
+ }
245
+
246
+ return this
247
+ }
248
+
249
+ /**
250
+ * Mark operation as successful
251
+ *
252
+ * @param {string} [text] - Optional success message (defaults to current text)
253
+ * @returns {Reporter} this instance for chaining
254
+ *
255
+ * @example
256
+ * reporter.start('Building...')
257
+ * await build()
258
+ * reporter.succeed('Build complete')
259
+ *
260
+ * @example Using current text
261
+ * reporter.start('Building site...')
262
+ * await build()
263
+ * reporter.succeed() // Shows: ✔ Building site...
264
+ */
265
+ succeed(text) {
266
+ this.#stopElapsedTimer()
267
+
268
+ const message = text || this.#baseText
269
+
270
+ if (this.#shouldSuppress()) {
271
+ debug('spinner succeed (suppressed): %s', message)
272
+ this.#spinner = null
273
+ return this
274
+ }
275
+
276
+ if (this.#spinner) {
277
+ debug('spinner succeed: %s', message)
278
+ this.#spinner.succeed(message)
279
+ this.#spinner = null
280
+ }
281
+
282
+ return this
283
+ }
284
+
285
+ /**
286
+ * Mark operation as failed
287
+ *
288
+ * @param {string} [text] - Optional failure message (defaults to current text)
289
+ * @returns {Reporter} this instance for chaining
290
+ *
291
+ * @example
292
+ * reporter.start('Generating PDF...')
293
+ * try {
294
+ * await generatePdf()
295
+ * reporter.succeed()
296
+ * } catch (error) {
297
+ * reporter.fail('PDF generation failed')
298
+ * throw error
299
+ * }
300
+ */
301
+ fail(text) {
302
+ this.#stopElapsedTimer()
303
+
304
+ const message = text || this.#baseText
305
+
306
+ if (this.#shouldSuppress()) {
307
+ debug('spinner fail (suppressed): %s', message)
308
+ this.#spinner = null
309
+ return this
310
+ }
311
+
312
+ if (this.#spinner) {
313
+ debug('spinner fail: %s', message)
314
+ this.#spinner.fail(message)
315
+ this.#spinner = null
316
+ }
317
+
318
+ return this
319
+ }
320
+
321
+ /**
322
+ * Mark operation with a warning
323
+ *
324
+ * @param {string} [text] - Optional warning message (defaults to current text)
325
+ * @returns {Reporter} this instance for chaining
326
+ *
327
+ * @example
328
+ * reporter.start('Checking dependencies...')
329
+ * if (outdatedDeps.length > 0) {
330
+ * reporter.warn('Some dependencies are outdated')
331
+ * } else {
332
+ * reporter.succeed('All dependencies up to date')
333
+ * }
334
+ */
335
+ warn(text) {
336
+ this.#stopElapsedTimer()
337
+
338
+ const message = text || this.#baseText
339
+
340
+ if (this.#shouldSuppress()) {
341
+ debug('spinner warn (suppressed): %s', message)
342
+ this.#spinner = null
343
+ return this
344
+ }
345
+
346
+ if (this.#spinner) {
347
+ debug('spinner warn: %s', message)
348
+ this.#spinner.warn(message)
349
+ this.#spinner = null
350
+ }
351
+
352
+ return this
353
+ }
354
+
355
+ /**
356
+ * Display an info message (non-spinner)
357
+ *
358
+ * @param {string} text - Info message
359
+ * @returns {Reporter} this instance for chaining
360
+ *
361
+ * @example
362
+ * reporter.info('Using cached dependencies')
363
+ */
364
+ info(text) {
365
+ this.#stopElapsedTimer()
366
+
367
+ if (this.#shouldSuppress()) {
368
+ debug('spinner info (suppressed): %s', text)
369
+ this.#spinner = null
370
+ return this
371
+ }
372
+
373
+ if (this.#spinner) {
374
+ debug('spinner info: %s', text)
375
+ this.#spinner.info(text)
376
+ this.#spinner = null
377
+ } else {
378
+ // Create temporary spinner just to show info
379
+ ora().info(text)
380
+ }
381
+
382
+ return this
383
+ }
384
+
385
+ /**
386
+ * Display a detail line (only shown in verbose mode)
387
+ *
388
+ * Use this to provide additional context that's useful for users
389
+ * who want more information but isn't essential for normal operation.
390
+ *
391
+ * @param {string} text - Detail text to display
392
+ * @returns {Reporter} this instance for chaining
393
+ *
394
+ * @example
395
+ * reporter.configure({ verbose: true })
396
+ * reporter.start('Building site...')
397
+ * reporter.detail('Output: _site/')
398
+ * reporter.detail('Processing 45 pages')
399
+ * reporter.succeed('Build complete')
400
+ */
401
+ detail(text) {
402
+ // Only show details in verbose mode
403
+ if (!this.#verbose || this.#shouldSuppress()) {
404
+ debug('detail (suppressed): %s', text)
405
+ return this
406
+ }
407
+
408
+ // Print detail with indentation below spinner
409
+ if (this.#spinner) {
410
+ // Temporarily stop spinner to print detail
411
+ const wasSpinning = this.#spinner.isSpinning
412
+ if (wasSpinning) {
413
+ this.#spinner.stop()
414
+ }
415
+ console.log(` ${text}`)
416
+ if (wasSpinning) {
417
+ this.#spinner.start()
418
+ }
419
+ } else {
420
+ console.log(` ${text}`)
421
+ }
422
+
423
+ debug('detail: %s', text)
424
+ return this
425
+ }
426
+
427
+ /**
428
+ * Check if verbose mode is enabled
429
+ *
430
+ * @returns {boolean} True if verbose mode is enabled
431
+ */
432
+ isVerbose() {
433
+ return this.#verbose
434
+ }
435
+
436
+ /**
437
+ * Stop spinner without status indicator
438
+ *
439
+ * Use this when an operation is interrupted or when you need
440
+ * to clear the spinner before other output.
441
+ *
442
+ * @returns {Reporter} this instance for chaining
443
+ *
444
+ * @example
445
+ * reporter.start('Processing...')
446
+ * if (shouldAbort) {
447
+ * reporter.stop()
448
+ * return
449
+ * }
450
+ */
451
+ stop() {
452
+ this.#stopElapsedTimer()
453
+
454
+ if (this.#spinner) {
455
+ debug('spinner stop')
456
+ this.#spinner.stop()
457
+ this.#spinner = null
458
+ }
459
+
460
+ return this
461
+ }
462
+
463
+ /**
464
+ * Check if spinner is currently active
465
+ *
466
+ * @returns {boolean} True if a spinner is running
467
+ *
468
+ * @example
469
+ * if (reporter.isSpinning()) {
470
+ * reporter.stop()
471
+ * }
472
+ */
473
+ isSpinning() {
474
+ return this.#spinner !== null && this.#spinner.isSpinning
475
+ }
476
+
477
+ /**
478
+ * Get elapsed time since spinner started
479
+ *
480
+ * @returns {number|null} Elapsed time in milliseconds, or null if not started
481
+ */
482
+ getElapsed() {
483
+ if (this.#startTime === null) {
484
+ return null
485
+ }
486
+ return Date.now() - this.#startTime
487
+ }
488
+
489
+ /**
490
+ * Reset reporter state
491
+ *
492
+ * Useful for testing or when reusing reporter across multiple operations.
493
+ *
494
+ * @returns {Reporter} this instance for chaining
495
+ */
496
+ reset() {
497
+ this.stop()
498
+ this.#quiet = false
499
+ this.#json = false
500
+ this.#verbose = false
501
+ this.#startTime = null
502
+ this.#baseText = ''
503
+ debug('reporter reset')
504
+ return this
505
+ }
506
+ }
507
+
508
+ // Export singleton instance
509
+ export default new Reporter()
510
+
511
+ // Export class for testing
512
+ export { Reporter }