@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,350 @@
1
+ import test from 'ava'
2
+ import { Volume, createFsFromVolume } from 'memfs'
3
+ import esmock from 'esmock'
4
+
5
+ test.beforeEach((t) => {
6
+ // Create in-memory file system
7
+ t.context.vol = new Volume()
8
+ t.context.fs = createFsFromVolume(t.context.vol)
9
+ })
10
+
11
+ test.afterEach.always((t) => {
12
+ // Clear in-memory file system
13
+ t.context.vol.reset()
14
+ })
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // getVersion tests
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ test('getVersion should read version from project version file', async (t) => {
21
+ const { fs, vol } = t.context
22
+
23
+ vol.fromJSON({
24
+ '/project/.quire': '1.0.0'
25
+ })
26
+
27
+ const { getVersion } = await esmock('./version.js', {
28
+ 'fs-extra': {
29
+ existsSync: (p) => fs.existsSync(p),
30
+ readFileSync: (p, opts) => fs.readFileSync(p, opts)
31
+ },
32
+ './paths.js': {
33
+ default: { getProjectRoot: () => '/project' }
34
+ },
35
+ '#lib/conf/config.js': {
36
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
37
+ }
38
+ })
39
+
40
+ const result = getVersion('/project')
41
+
42
+ t.is(result, '1.0.0', 'should return version string')
43
+ })
44
+
45
+ test('getVersion should use default project root when not provided', async (t) => {
46
+ const { fs, vol } = t.context
47
+
48
+ vol.fromJSON({
49
+ '/default/project/.quire': '2.0.0'
50
+ })
51
+
52
+ const { getVersion } = await esmock('./version.js', {
53
+ 'fs-extra': {
54
+ existsSync: (p) => fs.existsSync(p),
55
+ readFileSync: (p, opts) => fs.readFileSync(p, opts)
56
+ },
57
+ './paths.js': {
58
+ default: { getProjectRoot: () => '/default/project' }
59
+ },
60
+ '#lib/conf/config.js': {
61
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
62
+ }
63
+ })
64
+
65
+ const result = getVersion()
66
+
67
+ t.is(result, '2.0.0', 'should use default project root')
68
+ })
69
+
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+ // setVersion tests
72
+ // ─────────────────────────────────────────────────────────────────────────────
73
+
74
+ test('setVersion should create version file with quire11ty field', async (t) => {
75
+ const { fs, vol } = t.context
76
+
77
+ vol.fromJSON({
78
+ '/project/.gitkeep': ''
79
+ })
80
+
81
+ const { setVersion } = await esmock('./version.js', {
82
+ 'fs-extra': {
83
+ existsSync: (p) => fs.existsSync(p),
84
+ readFileSync: (p, opts) => fs.readFileSync(p, opts),
85
+ writeFileSync: (p, data) => fs.writeFileSync(p, data)
86
+ },
87
+ './paths.js': {
88
+ default: { getProjectRoot: () => '/project' }
89
+ },
90
+ '#lib/conf/config.js': {
91
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
92
+ }
93
+ })
94
+
95
+ setVersion('1.5.0', '/project')
96
+
97
+ const written = fs.readFileSync('/project/.quire', 'utf8')
98
+ const parsed = JSON.parse(written)
99
+
100
+ t.is(parsed.quire11ty, '1.5.0', 'should write version to quire11ty field')
101
+ })
102
+
103
+ test('setVersion should preserve existing version file metadata', async (t) => {
104
+ const { fs, vol } = t.context
105
+
106
+ const existingData = JSON.stringify({
107
+ cli: '1.0.0',
108
+ starter: 'https://example.com/starter@1.0.0',
109
+ quire11ty: '1.0.0'
110
+ })
111
+
112
+ vol.fromJSON({
113
+ '/project/.quire': existingData
114
+ })
115
+
116
+ const { setVersion } = await esmock('./version.js', {
117
+ 'fs-extra': {
118
+ existsSync: (p) => fs.existsSync(p),
119
+ readFileSync: (p, opts) => fs.readFileSync(p, opts),
120
+ writeFileSync: (p, data) => fs.writeFileSync(p, data)
121
+ },
122
+ './paths.js': {
123
+ default: { getProjectRoot: () => '/project' }
124
+ },
125
+ '#lib/conf/config.js': {
126
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
127
+ }
128
+ })
129
+
130
+ setVersion('2.0.0', '/project')
131
+
132
+ const written = fs.readFileSync('/project/.quire', 'utf8')
133
+ const parsed = JSON.parse(written)
134
+
135
+ t.is(parsed.quire11ty, '2.0.0', 'should update quire11ty version')
136
+ t.is(parsed.cli, '1.0.0', 'should preserve cli version')
137
+ t.is(parsed.starter, 'https://example.com/starter@1.0.0', 'should preserve starter info')
138
+ })
139
+
140
+ test('setVersion should handle legacy plain text version file', async (t) => {
141
+ const { fs, vol } = t.context
142
+
143
+ // Legacy format: just a version string, not JSON
144
+ vol.fromJSON({
145
+ '/project/.quire': '1.0.0-legacy'
146
+ })
147
+
148
+ const { setVersion } = await esmock('./version.js', {
149
+ 'fs-extra': {
150
+ existsSync: (p) => fs.existsSync(p),
151
+ readFileSync: (p, opts) => fs.readFileSync(p, opts),
152
+ writeFileSync: (p, data) => fs.writeFileSync(p, data)
153
+ },
154
+ './paths.js': {
155
+ default: { getProjectRoot: () => '/project' }
156
+ },
157
+ '#lib/conf/config.js': {
158
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
159
+ }
160
+ })
161
+
162
+ setVersion('2.0.0', '/project')
163
+
164
+ const written = fs.readFileSync('/project/.quire', 'utf8')
165
+ const parsed = JSON.parse(written)
166
+
167
+ t.is(parsed.quire11ty, '2.0.0', 'should migrate to new JSON format')
168
+ })
169
+
170
+ test('setVersion should use default project root when not provided', async (t) => {
171
+ const { fs, vol } = t.context
172
+
173
+ vol.fromJSON({
174
+ '/default/project/.gitkeep': ''
175
+ })
176
+
177
+ const { setVersion } = await esmock('./version.js', {
178
+ 'fs-extra': {
179
+ existsSync: (p) => fs.existsSync(p),
180
+ readFileSync: (p, opts) => fs.readFileSync(p, opts),
181
+ writeFileSync: (p, data) => fs.writeFileSync(p, data)
182
+ },
183
+ './paths.js': {
184
+ default: { getProjectRoot: () => '/default/project' }
185
+ },
186
+ '#lib/conf/config.js': {
187
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
188
+ }
189
+ })
190
+
191
+ setVersion('3.0.0')
192
+
193
+ const written = fs.readFileSync('/default/project/.quire', 'utf8')
194
+ const parsed = JSON.parse(written)
195
+
196
+ t.is(parsed.quire11ty, '3.0.0', 'should use default project root')
197
+ })
198
+
199
+ // ─────────────────────────────────────────────────────────────────────────────
200
+ // getVersionsFromStarter tests
201
+ // ─────────────────────────────────────────────────────────────────────────────
202
+
203
+ test('getVersionsFromStarter should read versions from package.json', async (t) => {
204
+ const { fs, vol } = t.context
205
+
206
+ const packageJson = JSON.stringify({
207
+ name: 'quire-starter-default',
208
+ version: '1.2.3',
209
+ peerDependencies: {
210
+ '@thegetty/quire-11ty': '^1.0.0'
211
+ }
212
+ })
213
+
214
+ vol.fromJSON({
215
+ '/project/package.json': packageJson
216
+ })
217
+
218
+ const { getVersionsFromStarter } = await esmock('./version.js', {
219
+ 'fs-extra': {
220
+ existsSync: (p) => fs.existsSync(p),
221
+ readFileSync: (p, opts) => fs.readFileSync(p, opts)
222
+ },
223
+ './paths.js': {
224
+ default: { getProjectRoot: () => '/project' }
225
+ },
226
+ '#lib/conf/config.js': {
227
+ default: { get: () => '.quire' }
228
+ }
229
+ })
230
+
231
+ const result = await getVersionsFromStarter('/project')
232
+
233
+ t.is(result.quire11tyVersion, '^1.0.0', 'should return quire-11ty version range')
234
+ t.is(result.starterVersion, '1.2.3', 'should return starter version')
235
+ })
236
+
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+ // writeVersionFile tests
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+
241
+ test('writeVersionFile should write JSON to version file', async (t) => {
242
+ const { fs, vol } = t.context
243
+
244
+ vol.fromJSON({
245
+ '/project/.gitkeep': ''
246
+ })
247
+
248
+ const { writeVersionFile } = await esmock('./version.js', {
249
+ 'fs-extra': {
250
+ existsSync: (p) => fs.existsSync(p),
251
+ readFileSync: (p, opts) => fs.readFileSync(p, opts),
252
+ writeFileSync: (p, data) => fs.writeFileSync(p, data)
253
+ },
254
+ './paths.js': {
255
+ default: { getProjectRoot: () => '/project' }
256
+ },
257
+ '#lib/conf/config.js': {
258
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
259
+ }
260
+ })
261
+
262
+ const versionInfo = { cli: '1.0.0', starter: 'test@1.0.0' }
263
+ writeVersionFile('/project', versionInfo)
264
+
265
+ const written = fs.readFileSync('/project/.quire', 'utf8')
266
+ const parsed = JSON.parse(written)
267
+
268
+ t.deepEqual(parsed, versionInfo, 'should write version info as JSON')
269
+ })
270
+
271
+ // ─────────────────────────────────────────────────────────────────────────────
272
+ // readVersionFile tests
273
+ // ─────────────────────────────────────────────────────────────────────────────
274
+
275
+ test('readVersionFile should return null when file does not exist', async (t) => {
276
+ const { fs, vol } = t.context
277
+
278
+ vol.fromJSON({
279
+ '/project/.gitkeep': ''
280
+ })
281
+
282
+ const { readVersionFile } = await esmock('./version.js', {
283
+ 'fs-extra': {
284
+ existsSync: (p) => fs.existsSync(p),
285
+ readFileSync: (p, opts) => fs.readFileSync(p, opts)
286
+ },
287
+ './paths.js': {
288
+ default: { getProjectRoot: () => '/project' }
289
+ },
290
+ '#lib/conf/config.js': {
291
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
292
+ }
293
+ })
294
+
295
+ const result = readVersionFile('/project')
296
+
297
+ t.is(result, null, 'should return null when file does not exist')
298
+ })
299
+
300
+ test('readVersionFile should parse JSON version file', async (t) => {
301
+ const { fs, vol } = t.context
302
+
303
+ const versionInfo = { cli: '1.0.0', quire11ty: '1.5.0' }
304
+ vol.fromJSON({
305
+ '/project/.quire': JSON.stringify(versionInfo)
306
+ })
307
+
308
+ const { readVersionFile } = await esmock('./version.js', {
309
+ 'fs-extra': {
310
+ existsSync: (p) => fs.existsSync(p),
311
+ readFileSync: (p, opts) => fs.readFileSync(p, opts)
312
+ },
313
+ './paths.js': {
314
+ default: { getProjectRoot: () => '/project' }
315
+ },
316
+ '#lib/conf/config.js': {
317
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
318
+ }
319
+ })
320
+
321
+ const result = readVersionFile('/project')
322
+
323
+ t.deepEqual(result, versionInfo, 'should parse and return version info')
324
+ })
325
+
326
+ test('readVersionFile should handle legacy plain text format', async (t) => {
327
+ const { fs, vol } = t.context
328
+
329
+ // Legacy format: plain version string
330
+ vol.fromJSON({
331
+ '/project/.quire': '1.0.0-legacy'
332
+ })
333
+
334
+ const { readVersionFile } = await esmock('./version.js', {
335
+ 'fs-extra': {
336
+ existsSync: (p) => fs.existsSync(p),
337
+ readFileSync: (p, opts) => fs.readFileSync(p, opts)
338
+ },
339
+ './paths.js': {
340
+ default: { getProjectRoot: () => '/project' }
341
+ },
342
+ '#lib/conf/config.js': {
343
+ default: { get: (key) => key === 'versionFile' ? '.quire' : null }
344
+ }
345
+ })
346
+
347
+ const result = readVersionFile('/project')
348
+
349
+ t.deepEqual(result, { version: '1.0.0-legacy' }, 'should return legacy format as object')
350
+ })
@@ -1,3 +1,212 @@
1
- ## CLI Reporter Module
1
+ # Reporter Module
2
2
 
3
- Th `Reporter` module implements a logging concern for the Quire CLI, collecting output from commands and managing render of messages to the console.
3
+ The Reporter module provides CLI progress feedback through spinners and status indicators.
4
+
5
+ ## Overview
6
+
7
+ This module wraps [ora](https://github.com/sindresorhus/ora) to provide:
8
+
9
+ - Consistent progress indicators across all CLI commands
10
+ - Automatic suppression in quiet mode, JSON mode, and non-TTY environments
11
+ - Elapsed time display for long-running operations
12
+ - Multi-phase operation support
13
+
14
+ ## Usage
15
+
16
+ ### Basic Usage
17
+
18
+ ```javascript
19
+ import reporter from '#lib/reporter/index.js'
20
+
21
+ // Start a spinner
22
+ reporter.start('Building site...')
23
+
24
+ // Do work...
25
+ await build()
26
+
27
+ // Mark as complete
28
+ reporter.succeed('Build complete')
29
+ ```
30
+
31
+ ### Multi-Phase Operations
32
+
33
+ ```javascript
34
+ reporter.start('Cloning starter project...')
35
+ await git.clone(starter, '.')
36
+
37
+ reporter.update('Initializing repository...')
38
+ await git.init()
39
+
40
+ reporter.update('Installing dependencies...')
41
+ await npm.install()
42
+
43
+ reporter.succeed('Project created')
44
+ ```
45
+
46
+ ### Error Handling
47
+
48
+ ```javascript
49
+ reporter.start('Generating PDF...')
50
+ try {
51
+ await generatePdf()
52
+ reporter.succeed('PDF generated')
53
+ } catch (error) {
54
+ reporter.fail('PDF generation failed')
55
+ throw error
56
+ }
57
+ ```
58
+
59
+ ### Elapsed Time Display
60
+
61
+ For operations with unpredictable duration:
62
+
63
+ ```javascript
64
+ reporter.start('Building site...', { showElapsed: true })
65
+ // Shows: ⠋ Building site... (12s)
66
+ await build()
67
+ reporter.succeed('Build complete')
68
+ ```
69
+
70
+ ### Configuration
71
+
72
+ Configure the reporter to respect command options:
73
+
74
+ ```javascript
75
+ async action(options) {
76
+ // Call at start of action() to respect --quiet, --json flags
77
+ reporter.configure(options)
78
+
79
+ reporter.start('Working...')
80
+ // ...
81
+ }
82
+ ```
83
+
84
+ | Option | Effect |
85
+ |--------|--------|
86
+ | `quiet: true` | Suppress all spinner output |
87
+ | `json: true` | Suppress spinner (JSON-only output) |
88
+ | `verbose: true` | (Reserved for future use) |
89
+
90
+ ## API Reference
91
+
92
+ ### `reporter.configure(options)`
93
+
94
+ Configure reporter for current command context.
95
+
96
+ ```javascript
97
+ reporter.configure({ quiet: true, json: false })
98
+ ```
99
+
100
+ ### `reporter.start(text, options)`
101
+
102
+ Start a new spinner.
103
+
104
+ | Parameter | Type | Description |
105
+ |-----------|------|-------------|
106
+ | `text` | `string` | Spinner text |
107
+ | `options.showElapsed` | `boolean` | Show elapsed time (default: `false`) |
108
+
109
+ ### `reporter.update(text)`
110
+
111
+ Update spinner text while running.
112
+
113
+ ### `reporter.succeed(text)`
114
+
115
+ Mark operation as successful. Uses current text if `text` not provided.
116
+
117
+ ### `reporter.fail(text)`
118
+
119
+ Mark operation as failed.
120
+
121
+ ### `reporter.warn(text)`
122
+
123
+ Mark operation with a warning.
124
+
125
+ ### `reporter.info(text)`
126
+
127
+ Display an info message.
128
+
129
+ ### `reporter.stop()`
130
+
131
+ Stop spinner without status indicator.
132
+
133
+ ### `reporter.isSpinning()`
134
+
135
+ Returns `true` if a spinner is currently running.
136
+
137
+ ### `reporter.getElapsed()`
138
+
139
+ Returns elapsed time in milliseconds since `start()`, or `null` if not started.
140
+
141
+ ### `reporter.reset()`
142
+
143
+ Reset all reporter state. Useful in tests.
144
+
145
+ ## Testing
146
+
147
+ ### Mocking in Tests
148
+
149
+ ```javascript
150
+ import esmock from 'esmock'
151
+ import sinon from 'sinon'
152
+
153
+ const mockReporter = {
154
+ start: sinon.stub().returnsThis(),
155
+ update: sinon.stub().returnsThis(),
156
+ succeed: sinon.stub().returnsThis(),
157
+ fail: sinon.stub().returnsThis(),
158
+ configure: sinon.stub().returnsThis(),
159
+ }
160
+
161
+ const MyCommand = await esmock('./mycommand.js', {
162
+ '#lib/reporter/index.js': { default: mockReporter }
163
+ })
164
+ ```
165
+
166
+ ### Verifying Calls
167
+
168
+ ```javascript
169
+ test('shows progress during build', async (t) => {
170
+ await command.action(options)
171
+
172
+ t.true(mockReporter.start.calledWith('Building...'))
173
+ t.true(mockReporter.succeed.calledWith('Build complete'))
174
+ })
175
+ ```
176
+
177
+ ## Design Decisions
178
+
179
+ ### Why ora?
180
+
181
+ Automatic supression of output when the `--quiet` or `--json` flags are set or TTY is detected support the four primary user-groups/use-cases: non-technical, developer, maintainer, and programmatic use.
182
+
183
+
184
+ | Factor | ora | Alternatives |
185
+ |--------|-----|--------------|
186
+ | Bundle size | 30.4 kB | cli-progress: 62.2 kB |
187
+ | Maintenance | Active | progress: 7 years dormant |
188
+ | Complexity | Simple | listr2: over-engineered |
189
+
190
+ ### Singleton Pattern
191
+
192
+ Follows the established pattern from `lib/logger`, `lib/git`, and `lib/npm`:
193
+
194
+ ```javascript
195
+ // Singleton export
196
+ export default new Reporter()
197
+
198
+ // Class export for testing
199
+ export { Reporter }
200
+ ```
201
+
202
+ ### Automatic TTY Detection
203
+
204
+ Spinners are automatically suppressed when:
205
+
206
+ 1. `--quiet` flag is set
207
+ 2. `--json` flag is set
208
+ 3. `process.stdout.isTTY` is `false` (CI environments, piped output)
209
+
210
+ ## Related
211
+
212
+ - [lib/logger](../logger/) - Logging abstraction (different concern)