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

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-CMsFCzhp.cjs} +22 -17
  16. package/dist/generate-CMsFCzhp.cjs.map +1 -0
  17. package/dist/{generate-BmulGxIM.js → generate-CNXTYKOV.js} +22 -17
  18. package/dist/generate-CNXTYKOV.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-Dpg8e1HN.cjs → init--vszxTGP.cjs} +6 -6
  25. package/dist/{init-Dpg8e1HN.cjs.map → init--vszxTGP.cjs.map} +1 -1
  26. package/dist/{init-BTp9if7K.js → init-BD4zelZR.js} +6 -6
  27. package/dist/{init-BTp9if7K.js.map → init-BD4zelZR.js.map} +1 -1
  28. package/dist/{mcp-C9RoU-Dg.js → mcp-CKrpwcmV.js} +6 -6
  29. package/dist/{mcp-C9RoU-Dg.js.map → mcp-CKrpwcmV.js.map} +1 -1
  30. package/dist/{mcp-wpl6sYYR.cjs → mcp-DSYDu-Tk.cjs} +6 -6
  31. package/dist/{mcp-wpl6sYYR.cjs.map → mcp-DSYDu-Tk.cjs.map} +1 -1
  32. package/dist/package-BFdHEmb9.js +6 -0
  33. package/dist/package-BFdHEmb9.js.map +1 -0
  34. package/dist/{package-iheSdfas.cjs → package-CwCIaoAJ.cjs} +2 -2
  35. package/dist/package-CwCIaoAJ.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-CIcMzR6i.js +1377 -0
  43. package/dist/run-CIcMzR6i.js.map +1 -0
  44. package/dist/run-CYnDu3ch.js +51 -0
  45. package/dist/run-CYnDu3ch.js.map +1 -0
  46. package/dist/run-DQZAMhO5.cjs +1380 -0
  47. package/dist/run-DQZAMhO5.cjs.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-BPHQK1kh.js} +6 -6
  57. package/dist/{validate-BfJoCxrC.js.map → validate-BPHQK1kh.js.map} +1 -1
  58. package/dist/{validate-DIDBROB2.cjs → validate-R-AOm0fm.cjs} +6 -6
  59. package/dist/{validate-DIDBROB2.cjs.map → validate-R-AOm0fm.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
@@ -2,49 +2,52 @@ import { relative } from 'node:path'
2
2
  import process from 'node:process'
3
3
  import { styleText } from 'node:util'
4
4
  import * as clack from '@clack/prompts'
5
- import { formatMs, formatMsWithColor, getIntro, toCause } from '@internals/utils'
6
- import { defineLogger, logLevel as logLevelMap } from '@kubb/core'
7
- import { getSummary } from '../utils/getSummary.ts'
8
- import { runHook } from '../utils/runHook.ts'
9
- import { ClackWritable } from '../utils/Writables.ts'
10
- import { buildProgressLine, formatCommandWithArgs, formatMessage } from './utils.ts'
5
+ import { formatMsWithColor, getElapsedMs, getIntro, toCause } from '@internals/utils'
6
+ import { Diagnostics, type KubbHooks, logLevel as logLevelMap } from '@kubb/core'
7
+ import { defineLogger } from './defineLogger.ts'
8
+ import { buildProgressLine, createProgressCounters, formatCommandWithArgs, formatMessage, recordPluginResult, resetProgressCounters } from './utils.ts'
11
9
 
12
10
  /**
13
- * TTY logger with beautiful UI and progress indicators for local development.
11
+ * TTY logger for local development, with spinners and progress bars.
14
12
  */
15
13
  export const clackLogger = defineLogger({
16
14
  name: 'clack',
17
15
  install(context, options) {
18
16
  const logLevel = options?.logLevel ?? logLevelMap.info
19
17
  const state = {
20
- totalPlugins: 0,
21
- completedPlugins: 0,
22
- failedPlugins: 0,
23
- totalFiles: 0,
24
- processedFiles: 0,
25
- hrStart: process.hrtime(),
18
+ ...createProgressCounters(),
26
19
  spinner: clack.spinner(),
27
20
  isSpinning: false,
21
+ runningPlugins: new Set<string>(),
28
22
  activeProgress: new Map<string, { interval?: NodeJS.Timeout; progressBar: clack.ProgressResult }>(),
23
+ activeHookLogs: new Map<string, { taskLog: ReturnType<typeof clack.taskLog>; hrStart: [number, number] }>(),
29
24
  }
30
25
 
31
- function reset() {
32
- for (const [_key, active] of state.activeProgress) {
26
+ // Clear every active progress bar's interval, stop it, and drop the map.
27
+ function stopActiveProgress() {
28
+ for (const [, active] of state.activeProgress) {
33
29
  if (active.interval) {
34
30
  clearInterval(active.interval)
35
31
  }
36
32
  active.progressBar?.stop()
37
33
  }
34
+ state.activeProgress.clear()
35
+ }
38
36
 
39
- state.totalPlugins = 0
40
- state.completedPlugins = 0
41
- state.failedPlugins = 0
42
- state.totalFiles = 0
43
- state.processedFiles = 0
44
- state.hrStart = process.hrtime()
37
+ function reset() {
38
+ stopActiveProgress()
39
+
40
+ resetProgressCounters(state)
45
41
  state.spinner = clack.spinner()
46
42
  state.isSpinning = false
47
- state.activeProgress.clear()
43
+ state.runningPlugins.clear()
44
+ state.activeHookLogs.clear()
45
+ }
46
+
47
+ // Label for the shared plugin bar, listing the plugins currently generating.
48
+ function pluginProgressText(): string {
49
+ const running = [...state.runningPlugins].map((name) => styleText('bold', name))
50
+ return getMessage(running.length > 0 ? `Generating ${running.join(', ')}` : 'Generating plugins')
48
51
  }
49
52
 
50
53
  function showProgressStep() {
@@ -62,12 +65,20 @@ export const clackLogger = defineLogger({
62
65
  return formatMessage(message, logLevel)
63
66
  }
64
67
 
65
- function startSpinner(text?: string) {
66
- state.spinner.start(text)
67
- state.isSpinning = true
68
+ // Registers a handler that prints a fixed step message, skipped at silent level.
69
+ function onStep<E extends keyof KubbHooks>(event: E, message: string): void {
70
+ context.on(event, () => {
71
+ if (logLevel <= logLevelMap.silent) {
72
+ return
73
+ }
74
+ clack.log.step(getMessage(message))
75
+ })
68
76
  }
69
77
 
70
78
  function stopSpinner(text?: string) {
79
+ if (!state.isSpinning) {
80
+ return
81
+ }
71
82
  state.spinner.stop(text)
72
83
  state.isSpinning = false
73
84
  }
@@ -77,13 +88,13 @@ export const clackLogger = defineLogger({
77
88
  return
78
89
  }
79
90
 
80
- const text = getMessage([styleText('blue', 'ℹ'), message, styleText('dim', info)].join(' '))
91
+ const text = getMessage([styleText('blue', 'ℹ'), message, info ? styleText('dim', info) : undefined].filter(Boolean).join(' '))
81
92
 
82
93
  if (state.isSpinning) {
83
94
  state.spinner.message(text)
84
- } else {
85
- clack.log.info(text)
95
+ return
86
96
  }
97
+ clack.log.info(text)
87
98
  })
88
99
 
89
100
  context.on('kubb:success', ({ message, info = '' }) => {
@@ -95,9 +106,9 @@ export const clackLogger = defineLogger({
95
106
 
96
107
  if (state.isSpinning) {
97
108
  stopSpinner(text)
98
- } else {
99
- clack.log.success(text)
109
+ return
100
110
  }
111
+ clack.log.success(text)
101
112
  })
102
113
 
103
114
  context.on('kubb:warn', ({ message, info }) => {
@@ -119,12 +130,12 @@ export const clackLogger = defineLogger({
119
130
 
120
131
  if (state.isSpinning) {
121
132
  stopSpinner(getMessage(text))
122
- } else {
123
- clack.log.error(getMessage(text))
133
+ return
124
134
  }
135
+ clack.log.error(getMessage(text))
125
136
 
126
- // Show stack trace in debug mode (first 3 frames)
127
- if (logLevel >= logLevelMap.debug && error.stack) {
137
+ // Show stack trace in verbose mode (first 3 frames)
138
+ if (logLevel >= logLevelMap.verbose && error.stack) {
128
139
  const frames = error.stack.split('\n').slice(1, 4)
129
140
  for (const frame of frames) {
130
141
  clack.log.message(getMessage(styleText('dim', frame.trim())))
@@ -141,14 +152,21 @@ export const clackLogger = defineLogger({
141
152
  }
142
153
  })
143
154
 
144
- context.on('kubb:version:new', ({ currentVersion, latestVersion }) => {
145
- if (logLevel <= logLevelMap.silent) {
155
+ context.on('kubb:diagnostic', ({ diagnostic }) => {
156
+ // Silent still surfaces errors so failures stay visible. It drops warnings and info.
157
+ if (logLevel <= logLevelMap.silent && diagnostic.severity !== 'error') {
146
158
  return
147
159
  }
148
160
 
149
- try {
161
+ stopSpinner()
162
+
163
+ // Stop any lingering progress UI so the multi-line block renders cleanly.
164
+ stopActiveProgress()
165
+
166
+ // The version-update notice keeps its own framed box instead of the diagnostic gutter.
167
+ if (Diagnostics.isUpdate(diagnostic)) {
150
168
  clack.box(
151
- `\`v${currentVersion}\` → \`v${latestVersion}\`
169
+ `\`v${diagnostic.currentVersion}\` → \`v${diagnostic.latestVersion}\`
152
170
  Run \`npm install -g @kubb/cli\` to update`,
153
171
  'Update available for `Kubb`',
154
172
  {
@@ -160,37 +178,21 @@ Run \`npm install -g @kubb/cli\` to update`,
160
178
  titleAlign: 'center',
161
179
  },
162
180
  )
163
- } catch {
164
- console.log(`Update available for Kubb: v${currentVersion} → v${latestVersion}`)
165
- console.log('Run `npm install -g @kubb/cli` to update')
166
- }
167
- })
168
181
 
169
- context.on('kubb:lifecycle:start', async ({ version }) => {
170
- console.log(`\n${getIntro({ title: 'The ultimate toolkit for working with APIs', description: 'Ready to start', version, areEyesOpen: true })}\n`)
171
-
172
- reset()
173
- })
174
-
175
- context.on('kubb:config:start', () => {
176
- if (logLevel <= logLevelMap.silent) {
177
182
  return
178
183
  }
179
184
 
180
- const text = getMessage('Configuration started')
181
-
182
- clack.intro(text)
183
- startSpinner(getMessage('Configuration loading'))
185
+ // Hand the severity glyph to clack as the gutter `symbol`, then let it draw the
186
+ // bar on each detail line via the default `secondarySymbol`. The headline and
187
+ // details carry their own colors, so clack only owns the gutter.
188
+ const { symbol, headline, details } = Diagnostics.format(diagnostic)
189
+ clack.log.message([headline, ...details], { symbol })
184
190
  })
185
191
 
186
- context.on('kubb:config:end', () => {
187
- if (logLevel <= logLevelMap.silent) {
188
- return
189
- }
190
-
191
- const text = getMessage('Configuration completed')
192
+ context.on('kubb:lifecycle:start', async ({ version }) => {
193
+ console.log(`\n${getIntro({ title: 'The meta framework for code generation', description: 'Ready to start', version, areEyesOpen: true })}\n`)
192
194
 
193
- clack.outro(text)
195
+ reset()
194
196
  })
195
197
 
196
198
  context.on('kubb:generation:start', ({ config }) => {
@@ -199,11 +201,17 @@ Run \`npm install -g @kubb/cli\` to update`,
199
201
  // Initialize progress tracking for this generation
200
202
  state.totalPlugins = config.plugins?.length ?? 0
201
203
 
204
+ if (logLevel <= logLevelMap.silent) {
205
+ return
206
+ }
207
+
202
208
  const text = getMessage(['Generation started', config.name ? `for ${styleText('dim', config.name)}` : undefined].filter(Boolean).join(' '))
203
209
 
204
210
  clack.intro(text)
205
211
  })
206
212
 
213
+ // Plugins run concurrently, so they share a single progress bar. A bar per plugin
214
+ // would make clack render them side by side and pile up keypress listeners.
207
215
  context.on('kubb:plugin:start', ({ plugin }) => {
208
216
  if (logLevel <= logLevelMap.silent) {
209
217
  return
@@ -211,50 +219,44 @@ Run \`npm install -g @kubb/cli\` to update`,
211
219
 
212
220
  stopSpinner()
213
221
 
222
+ state.runningPlugins.add(plugin.name)
223
+
224
+ const active = state.activeProgress.get('plugins')
225
+ if (active) {
226
+ active.progressBar.advance(0, pluginProgressText())
227
+ return
228
+ }
229
+
214
230
  const progressBar = clack.progress({
215
231
  style: 'block',
216
- max: 100,
232
+ max: Math.max(state.totalPlugins, 1),
217
233
  size: 30,
218
234
  })
219
- const text = getMessage(`Generating ${styleText('bold', plugin.name)}`)
220
- progressBar.start(text)
221
-
222
- const interval = setInterval(() => {
223
- progressBar.advance()
224
- }, 100)
225
-
226
- state.activeProgress.set(plugin.name, { progressBar, interval })
235
+ progressBar.start(pluginProgressText())
236
+ // Catch up to plugins already finished before this bar opened.
237
+ progressBar.advance(state.completedPlugins + state.failedPlugins, pluginProgressText())
238
+ state.activeProgress.set('plugins', { progressBar })
227
239
  })
228
240
 
229
- context.on('kubb:plugin:end', ({ plugin, duration, success }) => {
241
+ context.on('kubb:plugin:end', ({ plugin, success }) => {
230
242
  stopSpinner()
231
243
 
232
- const active = state.activeProgress.get(plugin.name)
244
+ const active = state.activeProgress.get('plugins')
233
245
 
234
246
  if (!active || logLevel === logLevelMap.silent) {
235
247
  return
236
248
  }
237
249
 
238
- clearInterval(active.interval)
250
+ state.runningPlugins.delete(plugin.name)
251
+ recordPluginResult(state, success)
252
+ active.progressBar.advance(1, pluginProgressText())
239
253
 
240
- if (success) {
241
- state.completedPlugins++
242
- } else {
243
- state.failedPlugins++
254
+ // Close the bar once nothing is generating, then print the progress step.
255
+ if (state.runningPlugins.size === 0) {
256
+ active.progressBar.stop(getMessage('Plugins generated'))
257
+ state.activeProgress.delete('plugins')
258
+ showProgressStep()
244
259
  }
245
-
246
- const durationStr = formatMsWithColor(duration)
247
- const text = getMessage(
248
- success
249
- ? `${styleText('bold', plugin.name)} completed in ${durationStr}`
250
- : `${styleText('bold', plugin.name)} failed in ${styleText('red', formatMs(duration))}`,
251
- )
252
-
253
- active.progressBar.stop(text)
254
- state.activeProgress.delete(plugin.name)
255
-
256
- // Show progress step after each plugin
257
- showProgressStep()
258
260
  })
259
261
 
260
262
  context.on('kubb:files:processing:start', ({ files }) => {
@@ -279,23 +281,20 @@ Run \`npm install -g @kubb/cli\` to update`,
279
281
  state.activeProgress.set('files', { progressBar })
280
282
  })
281
283
 
282
- context.on('kubb:file:processing:update', ({ file, config }) => {
284
+ context.on('kubb:files:processing:update', ({ files }) => {
283
285
  if (logLevel <= logLevelMap.silent) {
284
286
  return
285
287
  }
286
288
 
287
289
  stopSpinner()
288
290
 
289
- state.processedFiles++
290
-
291
- const text = `Writing ${relative(config.root, file.path)}`
292
291
  const active = state.activeProgress.get('files')
293
-
294
- if (!active) {
295
- return
292
+ for (const { file, config } of files) {
293
+ state.processedFiles++
294
+ if (active) {
295
+ active.progressBar.advance(undefined, `Writing ${relative(config.root, file.path)}`)
296
+ }
296
297
  }
297
-
298
- active.progressBar.advance(undefined, text)
299
298
  })
300
299
  context.on('kubb:files:processing:end', () => {
301
300
  if (logLevel <= logLevelMap.silent) {
@@ -319,135 +318,70 @@ Run \`npm install -g @kubb/cli\` to update`,
319
318
  })
320
319
 
321
320
  context.on('kubb:generation:end', ({ config }) => {
321
+ stopSpinner()
322
+
322
323
  const text = getMessage(config.name ? `Generation completed for ${styleText('dim', config.name)}` : 'Generation completed')
323
324
 
324
325
  clack.outro(text)
325
326
  })
326
327
 
327
- context.on('kubb:format:start', () => {
328
- if (logLevel <= logLevelMap.silent) {
329
- return
330
- }
331
-
332
- const text = getMessage('Format started')
333
-
334
- clack.intro(text)
335
- })
336
-
337
- context.on('kubb:format:end', () => {
338
- if (logLevel <= logLevelMap.silent) {
339
- return
340
- }
341
-
342
- const text = getMessage('Format completed')
343
-
344
- clack.outro(text)
345
- })
328
+ onStep('kubb:format:start', 'Formatting')
329
+ onStep('kubb:lint:start', 'Linting')
330
+ onStep('kubb:hooks:start', 'Running hooks')
346
331
 
347
- context.on('kubb:lint:start', () => {
348
- if (logLevel <= logLevelMap.silent) {
332
+ context.on('kubb:hook:start', ({ id, command, args }) => {
333
+ if (logLevel <= logLevelMap.silent || !id) {
349
334
  return
350
335
  }
351
336
 
352
- const text = getMessage('Lint started')
353
-
354
- clack.intro(text)
355
- })
356
-
357
- context.on('kubb:lint:end', () => {
358
- if (logLevel <= logLevelMap.silent) {
359
- return
360
- }
337
+ stopSpinner()
361
338
 
362
- const text = getMessage('Lint completed')
339
+ const commandWithArgs = formatCommandWithArgs(command, args)
340
+ const title = getMessage(`Running ${styleText('dim', commandWithArgs)}`)
341
+ const taskLog = clack.taskLog({ title })
363
342
 
364
- clack.outro(text)
343
+ state.activeHookLogs.set(id, { taskLog, hrStart: process.hrtime() })
365
344
  })
366
345
 
367
- context.on('kubb:hook:start', async ({ id, command, args }) => {
368
- const commandWithArgs = formatCommandWithArgs(command, args)
369
- const text = getMessage(`Hook ${styleText('dim', commandWithArgs)} started`)
346
+ // Registered only when not silent, so its presence is what tells the runner to stream
347
+ // (`kubb:hook:line` listenerCount). At silent level the listener is absent, so no streaming happens.
348
+ if (logLevel > logLevelMap.silent) {
349
+ context.on('kubb:hook:line', ({ id, line }) => {
350
+ const active = state.activeHookLogs.get(id)
351
+ active?.taskLog.message(styleText('dim', line))
352
+ })
353
+ }
370
354
 
371
- // Skip hook execution if no id is provided (e.g., during benchmarks or tests)
355
+ context.on('kubb:hook:end', ({ id, command, args, success, error, stdout, stderr }) => {
372
356
  if (!id) {
373
357
  return
374
358
  }
375
359
 
376
360
  if (logLevel <= logLevelMap.silent) {
377
- await runHook({
378
- id,
379
- command,
380
- args,
381
- commandWithArgs,
382
- context,
383
- sink: {
384
- onStderr: (s) => console.error(s),
385
- onStdout: (s) => console.log(s),
386
- },
387
- })
361
+ // Even when silent, surface a failed hook's captured output.
362
+ if (!success) {
363
+ if (stdout) console.log(stdout)
364
+ if (stderr) console.error(stderr)
365
+ }
388
366
  return
389
367
  }
390
368
 
391
- clack.intro(text)
392
-
393
- const logger = clack.taskLog({
394
- title: getMessage(['Executing hook', logLevel >= logLevelMap.info ? styleText('dim', commandWithArgs) : undefined].filter(Boolean).join(' ')),
395
- })
396
-
397
- const writable = new ClackWritable(logger)
398
-
399
- await runHook({
400
- id,
401
- command,
402
- args,
403
- commandWithArgs,
404
- context,
405
- stream: true,
406
- sink: {
407
- onLine: (line) => writable.write(line),
408
- onStderr: (s) => logger.error(s),
409
- onStdout: (s) => logger.message(s),
410
- },
411
- })
412
- })
413
-
414
- context.on('kubb:hook:end', ({ command, args }) => {
415
- if (logLevel <= logLevelMap.silent) {
369
+ const active = state.activeHookLogs.get(id)
370
+ if (!active) {
416
371
  return
417
372
  }
373
+ state.activeHookLogs.delete(id)
418
374
 
419
375
  const commandWithArgs = formatCommandWithArgs(command, args)
420
- const text = getMessage(`Hook ${styleText('dim', commandWithArgs)} successfully executed`)
421
-
422
- clack.outro(text)
423
- })
376
+ const duration = formatMsWithColor(getElapsedMs(active.hrStart))
424
377
 
425
- context.on('kubb:generation:summary', ({ config, pluginTimings, failedPlugins, filesCreated, status, hrStart }) => {
426
- const summary = getSummary({
427
- failedPlugins,
428
- filesCreated,
429
- config,
430
- status,
431
- hrStart,
432
- pluginTimings: logLevel >= logLevelMap.verbose ? pluginTimings : undefined,
433
- })
434
- const title = config.name || ''
435
-
436
- summary.unshift('\n')
437
- summary.push('\n')
438
-
439
- const borderColor = status === 'success' ? 'green' : 'red'
440
- try {
441
- clack.box(summary.join('\n'), getMessage(title), {
442
- width: 'auto',
443
- formatBorder: (s: string) => styleText(borderColor, s),
444
- rounded: true,
445
- withGuide: false,
446
- contentAlign: 'left',
447
- titleAlign: 'center',
448
- })
449
- } catch {
450
- console.log(summary.join('\n'))
378
+ if (success) {
379
+ active.taskLog.success(getMessage(`${styleText('dim', commandWithArgs)} completed in ${duration}`))
380
+ } else {
381
+ // The hook's output already reached the taskLog live via `kubb:hook:line`, so `showLog`
382
+ // replays it here. `kubb:hook:end` carries no captured output on the streaming path.
383
+ const reason = error?.message ? ` (${error.message})` : ''
384
+ active.taskLog.error(getMessage(`${styleText('dim', commandWithArgs)} failed${reason}`), { showLog: true })
451
385
  }
452
386
  })
453
387
 
@@ -0,0 +1,59 @@
1
+ import type { AsyncEventEmitter } from '@internals/utils'
2
+ import type { KubbHooks } from '@kubb/core'
3
+
4
+ /**
5
+ * Options accepted by a logger's `install` callback.
6
+ */
7
+ export type LoggerOptions = {
8
+ /**
9
+ * Output verbosity. Use the `logLevel` constants exported from `@kubb/core`
10
+ * (`silent`, `error`, `warn`, `info`, `verbose`, `debug`).
11
+ */
12
+ logLevel: number
13
+ }
14
+
15
+ /**
16
+ * Event emitter handed to `Logger.install`. Use `.on('kubb:info', ...)` and
17
+ * friends to subscribe to build events.
18
+ */
19
+ export type LoggerContext = AsyncEventEmitter<KubbHooks>
20
+
21
+ /**
22
+ * Logger contract. A logger receives the build's event emitter and subscribes
23
+ * to whichever lifecycle events it wants to forward to its destination
24
+ * (console, file, remote service).
25
+ */
26
+ export type Logger<TOptions extends LoggerOptions = LoggerOptions> = {
27
+ /**
28
+ * Display name used in diagnostics.
29
+ */
30
+ name: string
31
+ /**
32
+ * Called once per build with the shared event emitter. Subscribe to the
33
+ * lifecycle events the logger wants to forward to its destination.
34
+ */
35
+ install: (context: LoggerContext, options?: TOptions) => void | Promise<void>
36
+ }
37
+
38
+ export type UserLogger<TOptions extends LoggerOptions = LoggerOptions> = Logger<TOptions>
39
+
40
+ /**
41
+ * Defines a typed logger. The `install` method subscribes to lifecycle events
42
+ * on the shared emitter and forwards them to the logger's destination.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { defineLogger } from '@kubb/cli'
47
+ *
48
+ * export const myLogger = defineLogger({
49
+ * name: 'my-logger',
50
+ * install(context) {
51
+ * context.on('kubb:info', ({ message }) => console.log('ℹ', message))
52
+ * context.on('kubb:error', ({ error }) => console.error('✗', error.message))
53
+ * },
54
+ * })
55
+ * ```
56
+ */
57
+ export function defineLogger<Options extends LoggerOptions = LoggerOptions>(logger: UserLogger<Options>): Logger<Options> {
58
+ return logger
59
+ }