@kubb/cli 5.0.0-beta.6 → 5.0.0-beta.61

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 (142) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +170 -51
  3. package/dist/Telemetry-CVdyJarO.js +283 -0
  4. package/dist/Telemetry-CVdyJarO.js.map +1 -0
  5. package/dist/Telemetry-DrppRqqW.cjs +320 -0
  6. package/dist/Telemetry-DrppRqqW.cjs.map +1 -0
  7. package/dist/{define-Bdn8j5VM.cjs → define-C4AB3POr.cjs} +2 -2
  8. package/dist/{define-Bdn8j5VM.cjs.map → define-C4AB3POr.cjs.map} +1 -1
  9. package/dist/{define-Ctii4bel.js → define-C63T4jp6.js} +2 -2
  10. package/dist/{define-Ctii4bel.js.map → define-C63T4jp6.js.map} +1 -1
  11. package/dist/{errors-CjPmyZHy.js → errors-BsemQCMn.js} +2 -2
  12. package/dist/{errors-CjPmyZHy.js.map → errors-BsemQCMn.js.map} +1 -1
  13. package/dist/{errors-CLCjoSg0.cjs → errors-DykI11xo.cjs} +2 -2
  14. package/dist/{errors-CLCjoSg0.cjs.map → errors-DykI11xo.cjs.map} +1 -1
  15. package/dist/{generate-BB2Q7I9s.cjs → generate-CJuBlTeJ.cjs} +22 -17
  16. package/dist/generate-CJuBlTeJ.cjs.map +1 -0
  17. package/dist/{generate-BmulGxIM.js → generate-DAePC4G2.js} +22 -17
  18. package/dist/generate-DAePC4G2.js.map +1 -0
  19. package/dist/index.cjs +10 -27
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +10 -27
  23. package/dist/index.js.map +1 -1
  24. package/dist/{init-BTp9if7K.js → init-C8m7Z_W8.js} +6 -6
  25. package/dist/{init-BTp9if7K.js.map → init-C8m7Z_W8.js.map} +1 -1
  26. package/dist/{init-Dpg8e1HN.cjs → init-T1DC44PT.cjs} +6 -6
  27. package/dist/{init-Dpg8e1HN.cjs.map → init-T1DC44PT.cjs.map} +1 -1
  28. package/dist/{mcp-wpl6sYYR.cjs → mcp-B60MeTmD.cjs} +6 -6
  29. package/dist/{mcp-wpl6sYYR.cjs.map → mcp-B60MeTmD.cjs.map} +1 -1
  30. package/dist/{mcp-C9RoU-Dg.js → mcp-lEYflz1a.js} +6 -6
  31. package/dist/{mcp-C9RoU-Dg.js.map → mcp-lEYflz1a.js.map} +1 -1
  32. package/dist/package-BynuV_3t.js +6 -0
  33. package/dist/package-BynuV_3t.js.map +1 -0
  34. package/dist/{package-iheSdfas.cjs → package-QUV6JOtx.cjs} +2 -2
  35. package/dist/package-QUV6JOtx.cjs.map +1 -0
  36. package/dist/run-9ZhHuNZQ.cjs +33 -0
  37. package/dist/run-9ZhHuNZQ.cjs.map +1 -0
  38. package/dist/run-BG7Giryi.js +296 -0
  39. package/dist/run-BG7Giryi.js.map +1 -0
  40. package/dist/{validate-BU4fPTMc.cjs → run-BQO_tPlc.cjs} +25 -20
  41. package/dist/run-BQO_tPlc.cjs.map +1 -0
  42. package/dist/run-C-5oRB_f.cjs +1380 -0
  43. package/dist/run-C-5oRB_f.cjs.map +1 -0
  44. package/dist/run-CYnDu3ch.js +51 -0
  45. package/dist/run-CYnDu3ch.js.map +1 -0
  46. package/dist/run-Da0AZlgo.js +1377 -0
  47. package/dist/run-Da0AZlgo.js.map +1 -0
  48. package/dist/run-DpKny2hT.cjs +300 -0
  49. package/dist/run-DpKny2hT.cjs.map +1 -0
  50. package/dist/run-h8NTawHO.js +32 -0
  51. package/dist/run-h8NTawHO.js.map +1 -0
  52. package/dist/tools-BU99bhi8.js +152 -0
  53. package/dist/tools-BU99bhi8.js.map +1 -0
  54. package/dist/tools-_Xp8-_zy.cjs +175 -0
  55. package/dist/tools-_Xp8-_zy.cjs.map +1 -0
  56. package/dist/{validate-BfJoCxrC.js → validate-BhDi3x5e.js} +6 -6
  57. package/dist/{validate-BfJoCxrC.js.map → validate-BhDi3x5e.js.map} +1 -1
  58. package/dist/{validate-DIDBROB2.cjs → validate-CUEvWAyG.cjs} +6 -6
  59. package/dist/{validate-DIDBROB2.cjs.map → validate-CUEvWAyG.cjs.map} +1 -1
  60. package/package.json +12 -27
  61. package/src/Telemetry.ts +297 -0
  62. package/src/commands/generate.ts +18 -12
  63. package/src/commands/init.ts +2 -2
  64. package/src/commands/mcp.ts +2 -2
  65. package/src/commands/validate.ts +2 -2
  66. package/src/constants.ts +2 -35
  67. package/src/index.ts +5 -21
  68. package/src/loggers/clackLogger.ts +136 -202
  69. package/src/loggers/defineLogger.ts +59 -0
  70. package/src/loggers/plainLogger.ts +48 -103
  71. package/src/loggers/types.ts +6 -1
  72. package/src/loggers/utils.ts +167 -24
  73. package/src/runners/generate/run.ts +399 -0
  74. package/src/runners/generate/utils.ts +229 -0
  75. package/src/runners/{init.ts → init/run.ts} +81 -78
  76. package/src/runners/init/utils.ts +39 -0
  77. package/src/runners/mcp/run.ts +37 -0
  78. package/src/runners/{validate.ts → validate/run.ts} +25 -20
  79. package/dist/agent-BJEvbSiP.js +0 -68
  80. package/dist/agent-BJEvbSiP.js.map +0 -1
  81. package/dist/agent-CXNO6dgj.cjs +0 -70
  82. package/dist/agent-CXNO6dgj.cjs.map +0 -1
  83. package/dist/agent-D9CKYh4K.cjs +0 -122
  84. package/dist/agent-D9CKYh4K.cjs.map +0 -1
  85. package/dist/agent-VXKxLCho.js +0 -118
  86. package/dist/agent-VXKxLCho.js.map +0 -1
  87. package/dist/constants-BPJBMT_6.js +0 -45
  88. package/dist/constants-BPJBMT_6.js.map +0 -1
  89. package/dist/constants-BYGmiFs0.cjs +0 -139
  90. package/dist/constants-BYGmiFs0.cjs.map +0 -1
  91. package/dist/constants-DSJ-Xrbv.js +0 -116
  92. package/dist/constants-DSJ-Xrbv.js.map +0 -1
  93. package/dist/constants-Rcaqzyd-.cjs +0 -80
  94. package/dist/constants-Rcaqzyd-.cjs.map +0 -1
  95. package/dist/generate-BB2Q7I9s.cjs.map +0 -1
  96. package/dist/generate-B_p5dl68.cjs +0 -1755
  97. package/dist/generate-B_p5dl68.cjs.map +0 -1
  98. package/dist/generate-BmulGxIM.js.map +0 -1
  99. package/dist/generate-DAsdUw3z.js +0 -1752
  100. package/dist/generate-DAsdUw3z.js.map +0 -1
  101. package/dist/init-CJ80lKSP.cjs +0 -239
  102. package/dist/init-CJ80lKSP.cjs.map +0 -1
  103. package/dist/init-DCqcEq86.js +0 -235
  104. package/dist/init-DCqcEq86.js.map +0 -1
  105. package/dist/mcp-D1llTaRM.cjs +0 -50
  106. package/dist/mcp-D1llTaRM.cjs.map +0 -1
  107. package/dist/mcp-DNUw8nqb.js +0 -49
  108. package/dist/mcp-DNUw8nqb.js.map +0 -1
  109. package/dist/package-iheSdfas.cjs.map +0 -1
  110. package/dist/package-vLafMWCe.js +0 -6
  111. package/dist/package-vLafMWCe.js.map +0 -1
  112. package/dist/shell-475fQKaX.cjs +0 -62
  113. package/dist/shell-475fQKaX.cjs.map +0 -1
  114. package/dist/shell-DLzN4fRo.js +0 -51
  115. package/dist/shell-DLzN4fRo.js.map +0 -1
  116. package/dist/telemetry-BLX0NzRk.cjs +0 -282
  117. package/dist/telemetry-BLX0NzRk.cjs.map +0 -1
  118. package/dist/telemetry-juq4QBf7.js +0 -245
  119. package/dist/telemetry-juq4QBf7.js.map +0 -1
  120. package/dist/validate-BU4fPTMc.cjs.map +0 -1
  121. package/dist/validate-k9s_hFah.js +0 -46
  122. package/dist/validate-k9s_hFah.js.map +0 -1
  123. package/src/commands/agent/start.ts +0 -50
  124. package/src/commands/agent.ts +0 -10
  125. package/src/loggers/fileSystemLogger.ts +0 -138
  126. package/src/loggers/githubActionsLogger.ts +0 -379
  127. package/src/runners/agent.ts +0 -155
  128. package/src/runners/generate.ts +0 -333
  129. package/src/runners/mcp.ts +0 -56
  130. package/src/types.ts +0 -11
  131. package/src/utils/Writables.ts +0 -17
  132. package/src/utils/executeHooks.ts +0 -45
  133. package/src/utils/flags.ts +0 -9
  134. package/src/utils/getConfig.ts +0 -10
  135. package/src/utils/getCosmiConfig.ts +0 -75
  136. package/src/utils/getSummary.ts +0 -68
  137. package/src/utils/packageManager.ts +0 -23
  138. package/src/utils/runHook.ts +0 -91
  139. package/src/utils/telemetry.ts +0 -273
  140. package/src/utils/watcher.ts +0 -19
  141. /package/dist/{chunk-ByKO4r7w.cjs → chunk-Bx3C2hgW.cjs} +0 -0
  142. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
@@ -1,23 +1,32 @@
1
1
  import { relative } from 'node:path'
2
2
  import { formatMs, toCause } from '@internals/utils'
3
- import { defineLogger, logLevel as logLevelMap } from '@kubb/core'
4
- import { SUMMARY_SEPARATOR } from '../constants.ts'
5
- import { getSummary } from '../utils/getSummary.ts'
6
- import { runHook } from '../utils/runHook.ts'
7
- import { formatCommandWithArgs, formatMessage } from './utils.ts'
3
+ import { Diagnostics, type KubbHooks, logLevel as logLevelMap } from '@kubb/core'
4
+ import { defineLogger } from './defineLogger.ts'
5
+ import { createHookTimer, formatCommandWithArgs, formatMessage } from './utils.ts'
8
6
 
9
7
  /**
10
- * Plain console adapter for non-TTY environments with simple `console.log` output.
8
+ * Plain console adapter for non-TTY environments, built on `console.log`.
11
9
  */
12
10
  export const plainLogger = defineLogger({
13
11
  name: 'plain',
14
12
  install(context, options) {
15
13
  const logLevel = options?.logLevel ?? logLevelMap.info
14
+ const hookTimer = createHookTimer()
16
15
 
17
16
  function getMessage(message: string): string {
18
17
  return formatMessage(message, logLevel)
19
18
  }
20
19
 
20
+ // Registers a handler that logs a fixed message, skipped at silent level.
21
+ function onStep<E extends keyof KubbHooks>(event: E, message: string): void {
22
+ context.on(event, () => {
23
+ if (logLevel <= logLevelMap.silent) {
24
+ return
25
+ }
26
+ console.log(getMessage(message))
27
+ })
28
+ }
29
+
21
30
  context.on('kubb:info', ({ message, info }) => {
22
31
  if (logLevel <= logLevelMap.silent) {
23
32
  return
@@ -55,8 +64,8 @@ export const plainLogger = defineLogger({
55
64
 
56
65
  console.log(text)
57
66
 
58
- // Show stack trace in debug mode (first 3 frames)
59
- if (logLevel >= logLevelMap.debug && error.stack) {
67
+ // Show stack trace in verbose mode (first 3 frames)
68
+ if (logLevel >= logLevelMap.verbose && error.stack) {
60
69
  const frames = error.stack.split('\n').slice(1, 4)
61
70
  for (const frame of frames) {
62
71
  console.log(getMessage(frame.trim()))
@@ -73,28 +82,16 @@ export const plainLogger = defineLogger({
73
82
  }
74
83
  })
75
84
 
76
- context.on('kubb:lifecycle:start', () => {
77
- console.log('Kubb CLI 🧩')
78
- })
79
-
80
- context.on('kubb:config:start', () => {
81
- if (logLevel <= logLevelMap.silent) {
85
+ context.on('kubb:diagnostic', ({ diagnostic }) => {
86
+ // Silent still surfaces errors so failures stay visible. It drops warnings and info.
87
+ if (logLevel <= logLevelMap.silent && diagnostic.severity !== 'error') {
82
88
  return
83
89
  }
84
-
85
- const text = getMessage('Configuration started')
86
-
87
- console.log(text)
90
+ console.log(getMessage(Diagnostics.formatLines(diagnostic).join('\n')))
88
91
  })
89
92
 
90
- context.on('kubb:config:end', () => {
91
- if (logLevel <= logLevelMap.silent) {
92
- return
93
- }
94
-
95
- const text = getMessage('Configuration completed')
96
-
97
- console.log(text)
93
+ context.on('kubb:lifecycle:start', ({ version }) => {
94
+ console.log(`Kubb CLI v${version}`)
98
95
  })
99
96
 
100
97
  context.on('kubb:generation:start', () => {
@@ -133,14 +130,14 @@ export const plainLogger = defineLogger({
133
130
  console.log(text)
134
131
  })
135
132
 
136
- context.on('kubb:file:processing:update', ({ file, config }) => {
133
+ context.on('kubb:files:processing:update', ({ files }) => {
137
134
  if (logLevel <= logLevelMap.silent) {
138
135
  return
139
136
  }
140
137
 
141
- const text = getMessage(`Writing ${relative(config.root, file.path)}`)
142
-
143
- console.log(text)
138
+ for (const { file, config } of files) {
139
+ console.log(getMessage(`Writing ${relative(config.root, file.path)}`))
140
+ }
144
141
  })
145
142
 
146
143
  context.on('kubb:files:processing:end', () => {
@@ -159,96 +156,44 @@ export const plainLogger = defineLogger({
159
156
  console.log(text)
160
157
  })
161
158
 
162
- context.on('kubb:format:start', () => {
163
- if (logLevel <= logLevelMap.silent) {
164
- return
165
- }
166
-
167
- const text = getMessage('Format started')
159
+ onStep('kubb:format:start', 'Format started')
160
+ onStep('kubb:format:end', 'Format completed')
161
+ onStep('kubb:lint:start', 'Lint started')
162
+ onStep('kubb:lint:end', 'Lint completed')
163
+ onStep('kubb:hooks:start', 'Hooks started')
164
+ onStep('kubb:hooks:end', 'Hooks completed')
168
165
 
169
- console.log(text)
170
- })
171
-
172
- context.on('kubb:format:end', () => {
166
+ context.on('kubb:hook:start', ({ id, command, args }) => {
173
167
  if (logLevel <= logLevelMap.silent) {
174
168
  return
175
169
  }
176
170
 
177
- const text = getMessage('Format completed')
178
-
179
- console.log(text)
180
- })
181
-
182
- context.on('kubb:lint:start', () => {
183
- if (logLevel <= logLevelMap.silent) {
184
- return
171
+ if (id) {
172
+ hookTimer.start(id)
185
173
  }
186
174
 
187
- const text = getMessage('Lint started')
188
-
189
- console.log(text)
175
+ const commandWithArgs = formatCommandWithArgs(command, args)
176
+ console.log(getMessage(`Hook ${commandWithArgs} started`))
190
177
  })
191
178
 
192
- context.on('kubb:lint:end', () => {
179
+ context.on('kubb:hook:end', ({ id, command, args, success, error, stdout, stderr }) => {
193
180
  if (logLevel <= logLevelMap.silent) {
194
181
  return
195
182
  }
196
183
 
197
- const text = getMessage('Lint completed')
184
+ const ms = id ? hookTimer.end(id) : undefined
185
+ const durationStr = ms !== undefined ? ` in ${formatMs(ms)}` : ''
198
186
 
199
- console.log(text)
200
- })
201
-
202
- context.on('kubb:hook:start', async ({ id, command, args }) => {
203
187
  const commandWithArgs = formatCommandWithArgs(command, args)
204
- const text = getMessage(`Hook ${commandWithArgs} started`)
205
-
206
- if (logLevel > logLevelMap.silent) {
207
- console.log(text)
208
- }
209
-
210
- // Skip hook execution if no id is provided (e.g., during benchmarks or tests)
211
- if (!id) {
212
- return
213
- }
214
-
215
- await runHook({
216
- id,
217
- command,
218
- args,
219
- commandWithArgs,
220
- context,
221
- sink: {
222
- onStdout: logLevel > logLevelMap.silent ? (s) => console.log(s) : undefined,
223
- onStderr: logLevel > logLevelMap.silent ? (s) => console.error(s) : undefined,
224
- },
225
- })
226
- })
227
188
 
228
- context.on('kubb:hook:end', ({ command, args }) => {
229
- if (logLevel <= logLevelMap.silent) {
230
- return
189
+ if (success) {
190
+ console.log(getMessage(`✓ Hook ${commandWithArgs} completed${durationStr}`))
191
+ } else {
192
+ if (stdout) console.log(stdout)
193
+ if (stderr) console.error(stderr)
194
+ const reason = error?.message ? ` (${error.message})` : ''
195
+ console.log(getMessage(`✗ Hook ${commandWithArgs} failed${durationStr}${reason}`))
231
196
  }
232
-
233
- const commandWithArgs = formatCommandWithArgs(command, args)
234
- const text = getMessage(`Hook ${commandWithArgs} completed`)
235
-
236
- console.log(text)
237
- })
238
-
239
- context.on('kubb:generation:summary', ({ config, pluginTimings, status, hrStart, failedPlugins, filesCreated }) => {
240
- const summary = getSummary({
241
- failedPlugins,
242
- filesCreated,
243
- config,
244
- status,
245
- hrStart,
246
- pluginTimings: logLevel >= logLevelMap.verbose ? pluginTimings : undefined,
247
- })
248
-
249
- console.log(SUMMARY_SEPARATOR)
250
- console.log(summary.join('\n'))
251
- console.log(SUMMARY_SEPARATOR)
252
197
  })
253
198
  },
254
199
  })
@@ -1 +1,6 @@
1
- export type LoggerType = 'clack' | 'github-actions' | 'plain'
1
+ /**
2
+ * Logger adapter selected by `setupReporters` based on the runtime environment.
3
+ * - `'clack'`: TTY-aware output with spinners and progress bars.
4
+ * - `'plain'`: Plain `console.log` output for non-TTY environments.
5
+ */
6
+ export type LoggerType = 'clack' | 'plain'
@@ -1,10 +1,10 @@
1
+ import process from 'node:process'
1
2
  import { styleText } from 'node:util'
2
- import { canUseTTY, formatHrtime, isGitHubActions } from '@internals/utils'
3
- import type { Logger, LoggerContext, LoggerOptions } from '@kubb/core'
3
+ import { canUseTTY, formatHrtime, getElapsedMs } from '@internals/utils'
4
+ import type { Reporter, ReporterContext } from '@kubb/core'
4
5
  import { logLevel as logLevelMap } from '@kubb/core'
6
+ import type { Logger, LoggerContext, LoggerOptions } from './defineLogger.ts'
5
7
  import { clackLogger } from './clackLogger.ts'
6
- import { fileSystemLogger } from './fileSystemLogger.ts'
7
- import { githubActionsLogger } from './githubActionsLogger.ts'
8
8
  import { plainLogger } from './plainLogger.ts'
9
9
  import type { LoggerType } from './types.ts'
10
10
 
@@ -26,20 +26,38 @@ export function formatMessage(message: string, logLevel: number): string {
26
26
  }
27
27
 
28
28
  type ProgressState = {
29
+ /**
30
+ * Total number of plugins scheduled for this generation run.
31
+ */
29
32
  totalPlugins: number
33
+ /**
34
+ * Number of plugins that have finished without error.
35
+ */
30
36
  completedPlugins: number
37
+ /**
38
+ * Number of plugins that exited with an error.
39
+ */
31
40
  failedPlugins: number
41
+ /**
42
+ * Total number of files expected to be written.
43
+ */
32
44
  totalFiles: number
45
+ /**
46
+ * Number of files written so far.
47
+ */
33
48
  processedFiles: number
49
+ /**
50
+ * `process.hrtime()` snapshot taken at the start of generation, used to compute elapsed time.
51
+ */
34
52
  hrStart: [number, number]
35
53
  }
36
54
 
37
55
  /**
38
- * Build the progress summary line shared by clack and GitHub Actions loggers.
56
+ * Build the progress summary line shown by the clack logger.
39
57
  * Returns null when there is nothing to display.
40
58
  */
41
59
  export function buildProgressLine(state: ProgressState): string | null {
42
- const parts: string[] = []
60
+ const parts: Array<string> = []
43
61
  const duration = formatHrtime(state.hrStart)
44
62
 
45
63
  if (state.totalPlugins > 0) {
@@ -62,45 +80,170 @@ export function buildProgressLine(state: ProgressState): string | null {
62
80
  return parts.join(styleText('dim', ' | '))
63
81
  }
64
82
 
83
+ /**
84
+ * Creates the per-run progress counters used by the clack logger.
85
+ */
86
+ export function createProgressCounters(): ProgressState {
87
+ return {
88
+ totalPlugins: 0,
89
+ completedPlugins: 0,
90
+ failedPlugins: 0,
91
+ totalFiles: 0,
92
+ processedFiles: 0,
93
+ hrStart: process.hrtime(),
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Resets the progress counters in place at the start/end of a generation run.
99
+ */
100
+ export function resetProgressCounters(state: ProgressState): void {
101
+ state.totalPlugins = 0
102
+ state.completedPlugins = 0
103
+ state.failedPlugins = 0
104
+ state.totalFiles = 0
105
+ state.processedFiles = 0
106
+ state.hrStart = process.hrtime()
107
+ }
108
+
109
+ /**
110
+ * Records a finished plugin against the progress counters.
111
+ */
112
+ export function recordPluginResult(state: ProgressState, success: boolean): void {
113
+ if (success) {
114
+ state.completedPlugins++
115
+ } else {
116
+ state.failedPlugins++
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Tracks per-hook start times so a logger can report a hook's elapsed duration.
122
+ * Used by the plain logger, which keys timing by hook `id`.
123
+ */
124
+ export type HookTimer = {
125
+ start(id: string): void
126
+ /**
127
+ * Returns the elapsed milliseconds since `start(id)`, or `undefined` when no start was recorded.
128
+ */
129
+ end(id: string): number | undefined
130
+ clear(): void
131
+ }
132
+
133
+ /**
134
+ * Creates a {@link HookTimer} backed by a private `id → hrtime` map.
135
+ */
136
+ export function createHookTimer(): HookTimer {
137
+ const starts = new Map<string, [number, number]>()
138
+
139
+ return {
140
+ start(id: string): void {
141
+ starts.set(id, process.hrtime())
142
+ },
143
+ end(id: string): number | undefined {
144
+ const hrStart = starts.get(id)
145
+ if (!hrStart) {
146
+ return undefined
147
+ }
148
+ starts.delete(id)
149
+ return getElapsedMs(hrStart)
150
+ },
151
+ clear(): void {
152
+ starts.clear()
153
+ },
154
+ }
155
+ }
156
+
65
157
  /**
66
158
  * Join a command and its optional args into a single display string.
67
- * e.g. ("prettier", ["--write", "."]) → "prettier --write ."
159
+ *
160
+ * @example
161
+ * `formatCommandWithArgs('prettier', ['--write', '.']) → 'prettier --write .'`
68
162
  */
69
- export function formatCommandWithArgs(command: string, args?: readonly string[]): string {
163
+ export function formatCommandWithArgs(command: string, args?: ReadonlyArray<string>): string {
70
164
  return args?.length ? `${command} ${args.join(' ')}` : command
71
165
  }
72
166
 
73
167
  function detectLogger(): LoggerType {
74
- if (isGitHubActions()) {
75
- return 'github-actions'
76
- }
77
168
  if (canUseTTY()) {
78
169
  return 'clack'
79
170
  }
80
171
  return 'plain'
81
172
  }
82
173
 
83
- const logMapper = {
174
+ const logMapper: Record<LoggerType, Logger> = {
84
175
  clack: clackLogger,
85
176
  plain: plainLogger,
86
- 'github-actions': githubActionsLogger,
87
- } as const satisfies Record<LoggerType, Logger>
177
+ }
88
178
 
89
- export async function setupLogger(context: LoggerContext, { logLevel }: LoggerOptions): Promise<void> {
90
- const type = detectLogger()
179
+ /**
180
+ * Bridges a {@link Reporter} onto the run's event emitter: calls `report` with each config's
181
+ * {@link GenerationResult} on `kubb:generation:end`. The reporter never touches the emitter.
182
+ */
183
+ export function installReporter(context: LoggerContext, reporter: Reporter, ctx: ReporterContext): void {
184
+ context.on('kubb:generation:end', async ({ config, diagnostics = [], filesCreated = 0, status = 'success', hrStart = process.hrtime() }) => {
185
+ await reporter.report({ config, diagnostics, filesCreated, status, hrStart }, ctx)
186
+ })
91
187
 
92
- const logger = logMapper[type] as Logger
188
+ if (reporter.drain) {
189
+ context.on('kubb:lifecycle:end', () => reporter.drain?.(ctx))
190
+ }
191
+ }
93
192
 
94
- if (!logger) {
95
- throw new Error(`Unknown adapter type: ${type}`)
193
+ /**
194
+ * Installs the live logger (the TUI view) and the given reporters (the output). The reporters are
195
+ * already selected by the caller (the CLI maps `--reporter` to names via `selectReporters`). This
196
+ * only wires them. Loggers receive hook subprocess output through `kubb:hook:line` and the
197
+ * `stdout`/`stderr` on `kubb:hook:end`, so nothing is returned here.
198
+ *
199
+ * Loggers and reporters are independent: the `cli` reporter also activates the env logger summary.
200
+ * The `json` reporter owns stdout, so the live logger and the `cli` summary are suppressed whenever
201
+ * `json` is among the reporters, even if `cli` is also listed.
202
+ */
203
+ async function setupReporters(context: LoggerContext, { logLevel, reporters }: LoggerOptions & { reporters: ReadonlyArray<Reporter> }): Promise<void> {
204
+ const hasJson = reporters.some((reporter) => reporter.name === 'json')
205
+ const ctx: ReporterContext = { logLevel }
206
+
207
+ for (const reporter of reporters) {
208
+ if (reporter.name === 'cli') {
209
+ if (hasJson) {
210
+ continue
211
+ }
212
+ const type = detectLogger()
213
+ const logger = logMapper[type]
214
+ if (!logger) {
215
+ throw new Error(`Unknown adapter type: ${type}`)
216
+ }
217
+ await logger.install(context, { logLevel })
218
+ }
219
+
220
+ installReporter(context, reporter, ctx)
96
221
  }
222
+ }
97
223
 
98
- // Install primary logger
99
- const cleanup = await logger.install(context, { logLevel })
224
+ export default setupReporters
100
225
 
101
- if (logLevel >= logLevelMap.debug) {
102
- await fileSystemLogger.install(context, { logLevel })
226
+ /**
227
+ * Picks the reporters whose `name` matches one of `names`, in the order the names are given.
228
+ * The config carries every available reporter, and the host selects which to activate by name
229
+ * (the CLI maps `--reporter` to this). Duplicate names and names without a matching reporter are
230
+ * skipped.
231
+ */
232
+ export function selectReporters(reporters: ReadonlyArray<Reporter>, names: ReadonlyArray<string>): Array<Reporter> {
233
+ const seen = new Set<string>()
234
+ const selected: Array<Reporter> = []
235
+
236
+ for (const name of names) {
237
+ if (seen.has(name)) {
238
+ continue
239
+ }
240
+ seen.add(name)
241
+
242
+ const reporter = reporters.find((candidate) => candidate.name === name)
243
+ if (reporter) {
244
+ selected.push(reporter)
245
+ }
103
246
  }
104
247
 
105
- return cleanup
248
+ return selected
106
249
  }