@kubb/cli 5.0.0-beta.36 → 5.0.0-beta.38

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 (81) hide show
  1. package/README.md +14 -11
  2. package/dist/{agent-DajReUxm.cjs → agent-Bl8JwjMa.cjs} +4 -4
  3. package/dist/{agent-DajReUxm.cjs.map → agent-Bl8JwjMa.cjs.map} +1 -1
  4. package/dist/{agent-cAalLgeU.js → agent-CfZ_Uqde.js} +4 -4
  5. package/dist/{agent-cAalLgeU.js.map → agent-CfZ_Uqde.js.map} +1 -1
  6. package/dist/{constants-FhPsMOdo.cjs → constants-CAKUpLcQ.cjs} +3 -12
  7. package/dist/{constants-FhPsMOdo.cjs.map → constants-CAKUpLcQ.cjs.map} +1 -1
  8. package/dist/{constants-Co6NWt3U.js → constants-CYxk4aNm.js} +4 -7
  9. package/dist/{constants-Co6NWt3U.js.map → constants-CYxk4aNm.js.map} +1 -1
  10. package/dist/{generate-C6oskVzT.cjs → generate-Bgds6Zx3.cjs} +19 -14
  11. package/dist/generate-Bgds6Zx3.cjs.map +1 -0
  12. package/dist/{generate-DmYQJcBv.js → generate-CfxFqNeb.js} +19 -14
  13. package/dist/generate-CfxFqNeb.js.map +1 -0
  14. package/dist/index.cjs +8 -8
  15. package/dist/index.js +8 -8
  16. package/dist/{init-Dbb4U-Xs.cjs → init-C5wnuzeK.cjs} +2 -2
  17. package/dist/{init-Dbb4U-Xs.cjs.map → init-C5wnuzeK.cjs.map} +1 -1
  18. package/dist/{init-iOg_X-uh.js → init-TIec3Dym.js} +2 -2
  19. package/dist/{init-iOg_X-uh.js.map → init-TIec3Dym.js.map} +1 -1
  20. package/dist/{mcp-DxrSTT8i.cjs → mcp-Cr753GW1.cjs} +3 -3
  21. package/dist/{mcp-DxrSTT8i.cjs.map → mcp-Cr753GW1.cjs.map} +1 -1
  22. package/dist/{mcp-Ci2OkdBj.js → mcp-Damue5Mq.js} +3 -3
  23. package/dist/{mcp-Ci2OkdBj.js.map → mcp-Damue5Mq.js.map} +1 -1
  24. package/dist/package-Cnt1K03J.js +6 -0
  25. package/dist/package-Cnt1K03J.js.map +1 -0
  26. package/dist/{package-DbsOo2rT.cjs → package-guApEHiW.cjs} +2 -2
  27. package/dist/package-guApEHiW.cjs.map +1 -0
  28. package/dist/{run-v-75bcU1.js → run-BFEK9md9.js} +2 -2
  29. package/dist/{run-v-75bcU1.js.map → run-BFEK9md9.js.map} +1 -1
  30. package/dist/{run-GvXhj9XF.cjs → run-BFZtWpcW.cjs} +491 -314
  31. package/dist/run-BFZtWpcW.cjs.map +1 -0
  32. package/dist/{run-CCgNPz0F.cjs → run-BFv6avA_.cjs} +3 -3
  33. package/dist/{run-CCgNPz0F.cjs.map → run-BFv6avA_.cjs.map} +1 -1
  34. package/dist/{run-DpDKN_rb.cjs → run-BQZyg7If.cjs} +2 -2
  35. package/dist/{run-DpDKN_rb.cjs.map → run-BQZyg7If.cjs.map} +1 -1
  36. package/dist/{run-CPimpDgO.js → run-BvXxelGR.js} +2 -2
  37. package/dist/{run-CPimpDgO.js.map → run-BvXxelGR.js.map} +1 -1
  38. package/dist/{run-Lnupy7qb.cjs → run-Bz9IFMWg.cjs} +2 -2
  39. package/dist/{run-Lnupy7qb.cjs.map → run-Bz9IFMWg.cjs.map} +1 -1
  40. package/dist/{run-B9ZkldVt.js → run-C752fag9.js} +557 -380
  41. package/dist/run-C752fag9.js.map +1 -0
  42. package/dist/run-C_NMctua.cjs.map +1 -1
  43. package/dist/run-D8dCWepS.js.map +1 -1
  44. package/dist/{run-BRrNHp24.js → run-DJxYClJV.js} +3 -3
  45. package/dist/{run-BRrNHp24.js.map → run-DJxYClJV.js.map} +1 -1
  46. package/dist/{telemetry-DRhd3joO.cjs → telemetry-B80oJfxR.cjs} +2 -2
  47. package/dist/telemetry-B80oJfxR.cjs.map +1 -0
  48. package/dist/{telemetry-ne1IOrz1.js → telemetry-ueaMzs_c.js} +2 -2
  49. package/dist/telemetry-ueaMzs_c.js.map +1 -0
  50. package/dist/{validate-Bh7MgISX.js → validate-CYTKdezO.js} +3 -3
  51. package/dist/{validate-Bh7MgISX.js.map → validate-CYTKdezO.js.map} +1 -1
  52. package/dist/{validate-DVkJx4q8.cjs → validate-DMzjP-hd.cjs} +3 -3
  53. package/dist/{validate-DVkJx4q8.cjs.map → validate-DMzjP-hd.cjs.map} +1 -1
  54. package/package.json +6 -6
  55. package/src/commands/generate.ts +16 -10
  56. package/src/constants.ts +1 -1
  57. package/src/loggers/clackLogger.ts +68 -71
  58. package/src/loggers/diagnostics.ts +77 -0
  59. package/src/loggers/githubActionsLogger.ts +38 -31
  60. package/src/loggers/plainLogger.ts +10 -26
  61. package/src/loggers/types.ts +1 -1
  62. package/src/loggers/utils.ts +47 -94
  63. package/src/reporters/cliReporter.ts +89 -0
  64. package/src/reporters/fileReporter.ts +103 -0
  65. package/src/reporters/jsonReporter.ts +20 -0
  66. package/src/reporters/report.ts +84 -0
  67. package/src/runners/agent/run.ts +2 -2
  68. package/src/runners/generate/run.ts +130 -44
  69. package/src/runners/generate/utils.ts +8 -11
  70. package/src/runners/init/run.ts +1 -1
  71. package/src/telemetry.ts +2 -2
  72. package/dist/generate-C6oskVzT.cjs.map +0 -1
  73. package/dist/generate-DmYQJcBv.js.map +0 -1
  74. package/dist/package-DbsOo2rT.cjs.map +0 -1
  75. package/dist/package-_R15a7lY.js +0 -6
  76. package/dist/package-_R15a7lY.js.map +0 -1
  77. package/dist/run-B9ZkldVt.js.map +0 -1
  78. package/dist/run-GvXhj9XF.cjs.map +0 -1
  79. package/dist/telemetry-DRhd3joO.cjs.map +0 -1
  80. package/dist/telemetry-ne1IOrz1.js.map +0 -1
  81. package/src/loggers/fileSystemLogger.ts +0 -151
@@ -1,6 +1,6 @@
1
1
  import { styleText } from 'node:util'
2
- import { formatHrtime, formatMs, formatMsWithColor, toCause } from '@internals/utils'
3
- import { type Config, defineLogger, type KubbHooks, logLevel as logLevelMap } from '@kubb/core'
2
+ import { formatMs, formatMsWithColor, toCause } from '@internals/utils'
3
+ import { type Config, defineLogger, diagnosticCode, Diagnostics, isProblemDiagnostic, type KubbHooks, logLevel as logLevelMap } from '@kubb/core'
4
4
  import {
5
5
  buildProgressLine,
6
6
  createHookTimer,
@@ -127,14 +127,12 @@ export const githubActionsLogger = defineLogger({
127
127
  // (e.g., when getConfigs or kubb.setup throws) doesn't leak an open section.
128
128
  closeAllGroups()
129
129
 
130
- if (logLevel <= logLevelMap.silent) {
131
- return
132
- }
130
+ // Errors are always surfaced, even at silent, so failures stay visible.
133
131
  const message = error.message || String(error)
134
132
  console.error(`::error::${message}`)
135
133
 
136
- // Show stack trace in debug mode (first 3 frames)
137
- if (logLevel >= logLevelMap.debug && error.stack) {
134
+ // Show stack trace in verbose mode (first 3 frames)
135
+ if (logLevel >= logLevelMap.verbose && error.stack) {
138
136
  const frames = error.stack.split('\n').slice(1, 4)
139
137
  for (const frame of frames) {
140
138
  console.log(getMessage(styleText('dim', frame.trim())))
@@ -151,15 +149,40 @@ export const githubActionsLogger = defineLogger({
151
149
  }
152
150
  })
153
151
 
152
+ context.on('kubb:diagnostic', ({ diagnostic }) => {
153
+ closeAllGroups()
154
+
155
+ // Silent still surfaces errors so failures stay visible. It drops warnings and info.
156
+ if (logLevel <= logLevelMap.silent && diagnostic.severity !== 'error') {
157
+ return
158
+ }
159
+
160
+ if (!isProblemDiagnostic(diagnostic)) {
161
+ console.log(`::notice::${diagnostic.message}`)
162
+ return
163
+ }
164
+
165
+ const parts = [`${diagnostic.code} ${diagnostic.message}`]
166
+ if (diagnostic.location && 'pointer' in diagnostic.location) {
167
+ parts.push(`(at ${diagnostic.location.pointer})`)
168
+ }
169
+ if (diagnostic.plugin) {
170
+ parts.push(`[plugin: ${diagnostic.plugin}]`)
171
+ }
172
+ if (diagnostic.help) {
173
+ parts.push(`help: ${diagnostic.help}`)
174
+ }
175
+ if (diagnostic.code !== diagnosticCode.unknown) {
176
+ parts.push(`docs: ${Diagnostics.docsUrl(diagnostic.code)}`)
177
+ }
178
+ console.error(`::error::${parts.join(' ')}`)
179
+ })
180
+
154
181
  context.on('kubb:lifecycle:start', ({ version }) => {
155
182
  console.log(styleText('yellow', `Kubb ${version} 🧩`))
156
183
  reset()
157
184
  })
158
185
 
159
- context.on('kubb:version:new', ({ currentVersion, latestVersion }) => {
160
- console.log(`::notice::Update available for Kubb: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`)
161
- })
162
-
163
186
  context.on('kubb:config:start', () => {
164
187
  if (logLevel <= logLevelMap.silent) {
165
188
  return
@@ -289,6 +312,10 @@ export const githubActionsLogger = defineLogger({
289
312
  )
290
313
 
291
314
  console.log(text)
315
+
316
+ if (state.currentConfigs.length > 1) {
317
+ closeGroup(config.name ? `Generation for ${styleText('bold', config.name)}` : 'Generation')
318
+ }
292
319
  })
293
320
 
294
321
  onGroupStart('kubb:format:start', 'Format started', 'Formatting')
@@ -338,26 +365,6 @@ export const githubActionsLogger = defineLogger({
338
365
  }
339
366
  })
340
367
 
341
- context.on('kubb:generation:summary', ({ config, status, hrStart, failedPlugins }) => {
342
- const pluginsCount = config.plugins?.length ?? 0
343
- const successCount = pluginsCount - failedPlugins.size
344
- const duration = formatHrtime(hrStart)
345
-
346
- if (state.currentConfigs.length > 1) {
347
- console.log(' ')
348
- }
349
-
350
- console.log(
351
- status === 'success'
352
- ? `Kubb Summary: ${styleText('blue', '✓')} ${`${successCount} successful`}, ${pluginsCount} total, ${styleText('green', duration)}`
353
- : `Kubb Summary: ${styleText('blue', '✓')} ${`${successCount} successful`}, ✗ ${`${failedPlugins.size} failed`}, ${pluginsCount} total, ${styleText('green', duration)}`,
354
- )
355
-
356
- if (state.currentConfigs.length > 1) {
357
- closeGroup(config.name ? `Generation for ${styleText('bold', config.name)}` : 'Generation')
358
- }
359
- })
360
-
361
368
  context.on('kubb:lifecycle:end', () => {
362
369
  reset()
363
370
  })
@@ -1,8 +1,7 @@
1
1
  import { relative } from 'node:path'
2
2
  import { formatMs, toCause } from '@internals/utils'
3
3
  import { defineLogger, type KubbHooks, logLevel as logLevelMap } from '@kubb/core'
4
- import { SUMMARY_SEPARATOR } from '../constants.ts'
5
- import { getSummary } from './utils.ts'
4
+ import { formatDiagnostic } from './diagnostics.ts'
6
5
  import { createHookTimer, formatCommandWithArgs, formatMessage } from './utils.ts'
7
6
 
8
7
  /**
@@ -65,8 +64,8 @@ export const plainLogger = defineLogger({
65
64
 
66
65
  console.log(text)
67
66
 
68
- // Show stack trace in debug mode (first 3 frames)
69
- if (logLevel >= logLevelMap.debug && error.stack) {
67
+ // Show stack trace in verbose mode (first 3 frames)
68
+ if (logLevel >= logLevelMap.verbose && error.stack) {
70
69
  const frames = error.stack.split('\n').slice(1, 4)
71
70
  for (const frame of frames) {
72
71
  console.log(getMessage(frame.trim()))
@@ -83,16 +82,16 @@ export const plainLogger = defineLogger({
83
82
  }
84
83
  })
85
84
 
86
- context.on('kubb:lifecycle:start', ({ version }) => {
87
- console.log(`Kubb CLI v${version}`)
88
- })
89
-
90
- context.on('kubb:version:new', ({ currentVersion, latestVersion }) => {
91
- 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') {
92
88
  return
93
89
  }
90
+ console.log(getMessage(formatDiagnostic(diagnostic).join('\n')))
91
+ })
94
92
 
95
- console.log(getMessage(`Update available: v${currentVersion} v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`))
93
+ context.on('kubb:lifecycle:start', ({ version }) => {
94
+ console.log(`Kubb CLI v${version}`)
96
95
  })
97
96
 
98
97
  onStep('kubb:config:start', 'Configuration started')
@@ -198,21 +197,6 @@ export const plainLogger = defineLogger({
198
197
  }
199
198
  })
200
199
 
201
- context.on('kubb:generation:summary', ({ config, pluginTimings, status, hrStart, failedPlugins, filesCreated }) => {
202
- const summary = getSummary({
203
- failedPlugins,
204
- filesCreated,
205
- config,
206
- status,
207
- hrStart,
208
- pluginTimings: logLevel >= logLevelMap.verbose ? pluginTimings : undefined,
209
- })
210
-
211
- console.log(SUMMARY_SEPARATOR)
212
- console.log(summary.join('\n'))
213
- console.log(SUMMARY_SEPARATOR)
214
- })
215
-
216
200
  return (_commandWithArgs: string, _hookId: string) => ({
217
201
  onStdout: logLevel > logLevelMap.silent ? (s: string) => console.log(s) : undefined,
218
202
  onStderr: logLevel > logLevelMap.silent ? (s: string) => console.error(s) : undefined,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Logger adapter selected by `setupLogger` based on the runtime environment.
2
+ * Logger adapter selected by `setupReporters` based on the runtime environment.
3
3
  * - `'clack'`: TTY-aware output with spinners and progress bars.
4
4
  * - `'github-actions'`: CI output using `::group::` annotations.
5
5
  * - `'plain'`: Plain `console.log` output for non-TTY environments.
@@ -1,12 +1,12 @@
1
- import path from 'node:path'
2
1
  import process from 'node:process'
3
2
  import { styleText } from 'node:util'
4
- import { canUseTTY, formatHrtime, getElapsedMs, isGitHubActions, randomCliColor } from '@internals/utils'
5
- import type { Config, Logger, LoggerContext, LoggerOptions, Plugin } from '@kubb/core'
3
+ import { canUseTTY, formatHrtime, getElapsedMs, isGitHubActions } from '@internals/utils'
4
+ import type { Logger, LoggerContext, LoggerOptions, Reporter, ReporterContext, ReporterName } from '@kubb/core'
6
5
  import { logLevel as logLevelMap } from '@kubb/core'
7
- import { SUMMARY_MAX_BAR_LENGTH, SUMMARY_TIME_SCALE_DIVISOR } from '../constants.ts'
6
+ import { cliReporter } from '../reporters/cliReporter.ts'
7
+ import { fileReporter } from '../reporters/fileReporter.ts'
8
+ import { jsonReporter } from '../reporters/jsonReporter.ts'
8
9
  import { clackLogger } from './clackLogger.ts'
9
- import { fileSystemLogger } from './fileSystemLogger.ts'
10
10
  import { githubActionsLogger } from './githubActionsLogger.ts'
11
11
  import { plainLogger } from './plainLogger.ts'
12
12
  import type { LoggerType } from './types.ts'
@@ -226,105 +226,58 @@ const logMapper: Record<LoggerType, CLILogger> = {
226
226
  'github-actions': githubActionsLogger,
227
227
  }
228
228
 
229
- export async function setupLogger(context: LoggerContext, { logLevel }: LoggerOptions): Promise<HookSinkFactory | null> {
230
- const type = detectLogger()
231
-
232
- const logger = logMapper[type]
233
-
234
- if (!logger) {
235
- throw new Error(`Unknown adapter type: ${type}`)
236
- }
237
-
238
- const makeSink = await logger.install(context, { logLevel })
229
+ /**
230
+ * Bridges a {@link Reporter} onto the run's event emitter: calls `report` with each config's
231
+ * {@link GenerationResult} on `kubb:generation:end`. The reporter never touches the emitter.
232
+ */
233
+ export function installReporter(context: LoggerContext, reporter: Reporter, ctx: ReporterContext): void {
234
+ context.on('kubb:generation:end', async ({ config, diagnostics = [], filesCreated = 0, status = 'success', hrStart = process.hrtime() }) => {
235
+ await reporter.report({ config, diagnostics, filesCreated, status, hrStart }, ctx)
236
+ })
239
237
 
240
- if (logLevel >= logLevelMap.debug) {
241
- await fileSystemLogger.install(context, { logLevel })
238
+ if (reporter.flush) {
239
+ context.on('kubb:lifecycle:end', () => reporter.flush?.(ctx))
242
240
  }
243
-
244
- return typeof makeSink === 'function' ? makeSink : null
245
- }
246
-
247
- type SummaryProps = {
248
- /**
249
- * Set of plugins that failed during this generation run, each with its error.
250
- */
251
- failedPlugins: Set<{ plugin: Plugin; error: Error }>
252
- /**
253
- * Overall generation status used to choose success or failure formatting.
254
- */
255
- status: 'success' | 'failed'
256
- /**
257
- * `process.hrtime()` snapshot taken at the start of generation, used to compute elapsed time.
258
- */
259
- hrStart: [number, number]
260
- /**
261
- * Total number of files written during this generation run.
262
- */
263
- filesCreated: number
264
- /**
265
- * Resolved Kubb config for this generation entry, used to read plugin count and output path.
266
- */
267
- config: Config
268
- /**
269
- * Per-plugin timing map (plugin name → duration in ms). When provided, a timing bar chart is appended.
270
- */
271
- pluginTimings?: Map<string, number>
272
241
  }
273
242
 
274
243
  /**
275
- * Builds the generation summary lines rendered in the end-of-run box.
276
- * Returns an array of styled strings, one per summary row.
244
+ * Installs the live logger (the TUI view) and the selected reporters (the output), returning the
245
+ * terminal logger's hook sink when one was installed. Loggers and reporters are independent: the
246
+ * `cli` selection activates the env logger plus the {@link cliReporter} summary.
247
+ *
248
+ * The `json` reporter owns stdout, so the terminal logger and `cli` summary are suppressed whenever
249
+ * `json` is selected, even if `cli` is also listed.
277
250
  */
278
- export function getSummary({ failedPlugins, filesCreated, status, hrStart, config, pluginTimings }: SummaryProps): Array<string> {
279
- const duration = formatHrtime(hrStart)
280
-
281
- const pluginsCount = config.plugins?.length ?? 0
282
- const successCount = pluginsCount - failedPlugins.size
283
-
284
- const meta = {
285
- plugins:
286
- status === 'success'
287
- ? `${styleText('green', `${successCount} successful`)}, ${pluginsCount} total`
288
- : `${styleText('green', `${successCount} successful`)}, ${styleText('red', `${failedPlugins.size} failed`)}, ${pluginsCount} total`,
289
- pluginsFailed: status === 'failed' ? [...failedPlugins].map(({ plugin }) => randomCliColor(plugin.name)).join(', ') : undefined,
290
- filesCreated,
291
- time: styleText('green', duration),
292
- output: path.resolve(config.root, config.output.path),
293
- } as const
294
-
295
- const labels = {
296
- plugins: 'Plugins:',
297
- failed: 'Failed:',
298
- generated: 'Generated:',
299
- pluginTimings: 'Plugin Timings:',
300
- output: 'Output:',
251
+ export async function setupReporters(
252
+ context: LoggerContext,
253
+ { logLevel, reporters }: LoggerOptions & { reporters: ReadonlyArray<ReporterName> },
254
+ ): Promise<HookSinkFactory | null> {
255
+ const unique = new Set<ReporterName>(reporters.length ? reporters : ['cli'])
256
+ const hasJson = unique.has('json')
257
+ const ctx: ReporterContext = { logLevel }
258
+
259
+ let makeSink: HookSinkFactory | null = null
260
+
261
+ if (unique.has('cli') && !hasJson) {
262
+ const type = detectLogger()
263
+ const logger = logMapper[type]
264
+ if (!logger) {
265
+ throw new Error(`Unknown adapter type: ${type}`)
266
+ }
267
+ const sink = await logger.install(context, { logLevel })
268
+ makeSink = typeof sink === 'function' ? sink : null
269
+ installReporter(context, cliReporter, ctx)
301
270
  }
302
- const maxLength = Math.max(0, ...[...Object.values(labels), ...(pluginTimings ? Array.from(pluginTimings.keys()) : [])].map((s) => s.length))
303
-
304
- const summaryLines: Array<string> = []
305
- summaryLines.push(`${labels.plugins.padEnd(maxLength + 2)} ${meta.plugins}`)
306
271
 
307
- if (meta.pluginsFailed) {
308
- summaryLines.push(`${labels.failed.padEnd(maxLength + 2)} ${meta.pluginsFailed}`)
272
+ if (hasJson) {
273
+ // json aggregates across configs: report buffers each result and flush writes one array on
274
+ // lifecycle end, rather than printing per config (which would concatenate documents and break `jq .`).
275
+ installReporter(context, jsonReporter, ctx)
309
276
  }
310
277
 
311
- summaryLines.push(`${labels.generated.padEnd(maxLength + 2)} ${meta.filesCreated} files in ${meta.time}`)
312
-
313
- if (pluginTimings && pluginTimings.size > 0) {
314
- const sortedTimings = Array.from(pluginTimings.entries()).sort((a, b) => b[1] - a[1])
315
-
316
- summaryLines.push(`${labels.pluginTimings}`)
317
-
318
- sortedTimings.forEach(([name, time]) => {
319
- const timeStr = time >= 1000 ? `${(time / 1000).toFixed(2)}s` : `${Math.round(time)}ms`
320
- const barLength = Math.min(Math.ceil(time / SUMMARY_TIME_SCALE_DIVISOR), SUMMARY_MAX_BAR_LENGTH)
321
- const bar = styleText('dim', '█'.repeat(barLength))
322
-
323
- summaryLines.push(`${styleText('dim', '•')} ${name.padEnd(maxLength + 1)}${bar} ${timeStr}`)
324
- })
278
+ if (unique.has('file')) {
279
+ installReporter(context, fileReporter, ctx)
325
280
  }
326
281
 
327
- summaryLines.push(`${labels.output.padEnd(maxLength + 2)} ${meta.output}`)
328
-
329
- return summaryLines
282
+ return makeSink
330
283
  }
@@ -0,0 +1,89 @@
1
+ import { styleText } from 'node:util'
2
+ import { formatMs, randomCliColor } from '@internals/utils'
3
+ import { createReporter, logLevel as logLevelMap } from '@kubb/core'
4
+ import { SUMMARY_MAX_BAR_LENGTH, SUMMARY_TIME_SCALE_DIVISOR } from '../constants.ts'
5
+ import { buildReport, type Report } from './report.ts'
6
+
7
+ /**
8
+ * Builds the vitest/jest-style summary for one {@link Report}: right-aligned dim labels with
9
+ * `N passed (total)` counts, and a per-plugin `Timings` section when `showTimings`.
10
+ */
11
+ function buildSummaryLines(report: Report, { showTimings }: { showTimings: boolean }): Array<string> {
12
+ const { status, plugins, counts, filesCreated, durationMs, output, timings } = report
13
+
14
+ const rows: Array<[label: string, value: string]> = []
15
+
16
+ rows.push([
17
+ 'Plugins',
18
+ status === 'success'
19
+ ? `${styleText('green', `${plugins.passed} passed`)} (${plugins.total})`
20
+ : `${styleText('green', `${plugins.passed} passed`)} | ${styleText('red', `${plugins.failed.length} failed`)} (${plugins.total})`,
21
+ ])
22
+
23
+ if (status === 'failed' && plugins.failed.length > 0) {
24
+ rows.push(['Failed', plugins.failed.map((name) => randomCliColor(name)).join(', ')])
25
+ }
26
+
27
+ if (counts.errors > 0 || counts.warnings > 0) {
28
+ const issues = [
29
+ counts.errors > 0 ? styleText('red', `${counts.errors} ${counts.errors === 1 ? 'error' : 'errors'}`) : undefined,
30
+ counts.warnings > 0 ? styleText('yellow', `${counts.warnings} ${counts.warnings === 1 ? 'warning' : 'warnings'}`) : undefined,
31
+ ]
32
+ .filter(Boolean)
33
+ .join(' | ')
34
+ rows.push(['Issues', issues])
35
+ }
36
+
37
+ rows.push(['Files', `${styleText('green', String(filesCreated))} generated`])
38
+ rows.push(['Duration', styleText('green', formatMs(durationMs))])
39
+ rows.push(['Output', output])
40
+
41
+ const labelWidth = Math.max(...rows.map(([label]) => label.length), timings.length > 0 ? 'Timings'.length : 0)
42
+ const lines = rows.map(([label, value]) => `${styleText('dim', label.padStart(labelWidth))} ${value}`)
43
+
44
+ if (showTimings && timings.length > 0) {
45
+ const nameWidth = Math.max(0, ...timings.map((timing) => timing.plugin.length))
46
+ const indent = ' '.repeat(labelWidth + 2)
47
+
48
+ lines.push(styleText('dim', 'Timings'.padStart(labelWidth)))
49
+ for (const timing of timings) {
50
+ const timeStr = formatMs(timing.durationMs)
51
+ const barLength = Math.min(Math.ceil(timing.durationMs / SUMMARY_TIME_SCALE_DIVISOR), SUMMARY_MAX_BAR_LENGTH)
52
+ const bar = styleText('dim', '█'.repeat(barLength))
53
+ lines.push(`${indent}${styleText('dim', '•')} ${timing.plugin.padEnd(nameWidth)} ${bar} ${timeStr}`)
54
+ }
55
+ }
56
+
57
+ return lines
58
+ }
59
+
60
+ /**
61
+ * Renders the summary as plain `console.log` lines so it works in every CLI (no clack/TTY
62
+ * dependency): a blank line, the config name colored by status, then the summary rows.
63
+ */
64
+ function renderSummary(lines: ReadonlyArray<string>, { title, status }: { title: string; status: 'success' | 'failed' }): void {
65
+ console.log('')
66
+ if (title) {
67
+ console.log(styleText(status === 'failed' ? 'red' : 'green', title))
68
+ }
69
+ for (const line of lines) {
70
+ console.log(line)
71
+ }
72
+ }
73
+
74
+ /**
75
+ * The default `cli` reporter. Renders the {@link Report} for each config as it finishes, independent
76
+ * of the live logger view. Suppressed at `silent`; the `verbose` level adds the per-plugin timings.
77
+ */
78
+ export const cliReporter = createReporter({
79
+ name: 'cli',
80
+ report(result, { logLevel }) {
81
+ if (logLevel <= logLevelMap.silent) {
82
+ return
83
+ }
84
+
85
+ const report = buildReport(result)
86
+ const lines = buildSummaryLines(report, { showTimings: logLevel >= logLevelMap.verbose })
87
+ renderSummary(lines, { title: report.name, status: report.status })
88
+ },
89
+ })
@@ -0,0 +1,103 @@
1
+ import { relative, resolve } from 'node:path'
2
+ import process from 'node:process'
3
+ import { stripVTControlCharacters } from 'node:util'
4
+ import { formatMs, write } from '@internals/utils'
5
+ import { createReporter, type Diagnostic, isProblemDiagnostic } from '@kubb/core'
6
+ import { formatDiagnostic } from '../loggers/diagnostics.ts'
7
+ import { buildReport, type Report } from './report.ts'
8
+
9
+ /**
10
+ * Builds the `## Summary` section: the same counts the cli and json reporters expose, as a list of
11
+ * `label value` rows with the labels padded to a common width.
12
+ */
13
+ function buildSummarySection(report: Report): Array<string> {
14
+ const { status, plugins, counts, filesCreated, durationMs, output } = report
15
+
16
+ const rows: Array<[label: string, value: string]> = [
17
+ ['Status', status],
18
+ [
19
+ 'Plugins',
20
+ status === 'success' ? `${plugins.passed} passed (${plugins.total})` : `${plugins.passed} passed | ${plugins.failed.length} failed (${plugins.total})`,
21
+ ],
22
+ ]
23
+
24
+ if (plugins.failed.length > 0) {
25
+ rows.push(['Failed', plugins.failed.join(', ')])
26
+ }
27
+
28
+ rows.push(['Issues', `${counts.errors} errors | ${counts.warnings} warnings | ${counts.infos} infos`])
29
+ rows.push(['Files', `${filesCreated} generated`])
30
+ rows.push(['Duration', formatMs(durationMs)])
31
+ rows.push(['Output', output])
32
+
33
+ const labelWidth = Math.max(...rows.map(([label]) => label.length))
34
+ const lines = rows.map(([label, value]) => ` ${label.padEnd(labelWidth)} ${value}`)
35
+
36
+ return ['## Summary', '', ...lines]
37
+ }
38
+
39
+ /**
40
+ * Builds the `## Problems` section: each problem rendered in the miette block format, blocks
41
+ * separated by a blank line. Returns an empty array when there are no problems, so the caller
42
+ * can drop the heading.
43
+ */
44
+ function buildProblemSection(diagnostics: ReadonlyArray<Diagnostic>): Array<string> {
45
+ const problems = diagnostics.filter(isProblemDiagnostic)
46
+ if (problems.length === 0) {
47
+ return []
48
+ }
49
+
50
+ const blocks = problems.map((diagnostic) => formatDiagnostic(diagnostic).join('\n'))
51
+ return ['## Problems', '', blocks.join('\n\n')]
52
+ }
53
+
54
+ /**
55
+ * Builds the `## Timings` section from a {@link Report}: one `plugin duration` row per record,
56
+ * slowest first with the plugin names left-aligned and the durations right-aligned. Returns an
57
+ * empty array when there are no timings.
58
+ */
59
+ function buildTimingSection(report: Report): Array<string> {
60
+ const { timings } = report
61
+ if (timings.length === 0) {
62
+ return []
63
+ }
64
+
65
+ const nameWidth = Math.max(...timings.map((timing) => timing.plugin.length))
66
+ const durations = timings.map((timing) => formatMs(timing.durationMs))
67
+ const durationWidth = Math.max(...durations.map((duration) => duration.length))
68
+ const rows = timings.map((timing, index) => ` ${timing.plugin.padEnd(nameWidth)} ${durations[index]!.padStart(durationWidth)}`)
69
+
70
+ return ['## Timings', '', ...rows]
71
+ }
72
+
73
+ /**
74
+ * The `file` reporter. Writes a config's {@link Report} to `.kubb/kubb-<name>-<timestamp>.log` as a
75
+ * plain-text document: a `# <name> — <timestamp>` header, a `## Summary` with the same counts the
76
+ * cli and json reporters expose, a `## Problems` section in the miette block format, and a
77
+ * `## Timings` section. Selected with `--reporter file` (or `reporters: ['file']`), replacing the
78
+ * old `--debug` flag.
79
+ *
80
+ * @note Unlike the streaming logger it replaced, it captures the collected diagnostics once a
81
+ * config finishes, not the live `kubb:info`/`kubb:plugin` event stream. Color is stripped so the
82
+ * file stays plain text even when the run is attached to a TTY.
83
+ */
84
+ export const fileReporter = createReporter({
85
+ name: 'file',
86
+ async report(result) {
87
+ const { diagnostics, config } = result
88
+ if (diagnostics.length === 0) {
89
+ return
90
+ }
91
+
92
+ const report = buildReport(result)
93
+ const header = config.name ? `# ${config.name} — ${new Date().toISOString()}` : `# ${new Date().toISOString()}`
94
+ const sections = [buildSummarySection(report), buildProblemSection(diagnostics), buildTimingSection(report)].filter((section) => section.length > 0)
95
+ const content = stripVTControlCharacters([header, ...sections.map((section) => section.join('\n'))].join('\n\n'))
96
+
97
+ const baseName = `${['kubb', config.name, Date.now()].filter(Boolean).join('-')}.log`
98
+ const pathName = resolve(process.cwd(), '.kubb', baseName)
99
+
100
+ await write(pathName, `${content}\n`)
101
+ console.error(`Debug log written to ${relative(process.cwd(), pathName)}`)
102
+ },
103
+ })
@@ -0,0 +1,20 @@
1
+ import process from 'node:process'
2
+ import { createReporter } from '@kubb/core'
3
+ import { buildReport } from './report.ts'
4
+
5
+ /**
6
+ * The `json` reporter. `report` returns one config's {@link Report}, which {@link createReporter}
7
+ * buffers, and `flush` writes them as a single pretty-printed JSON array on `kubb:lifecycle:end`.
8
+ * Buffering keeps a multi-config run one valid JSON document on stdout instead of concatenated
9
+ * objects that would break `jq .`. The terminal reporter is suppressed while `json` is active so
10
+ * stdout stays valid JSON.
11
+ */
12
+ export const jsonReporter = createReporter({
13
+ name: 'json',
14
+ report(result) {
15
+ return buildReport(result)
16
+ },
17
+ flush(_context, reports) {
18
+ process.stdout.write(`${JSON.stringify(reports, null, 2)}\n`)
19
+ },
20
+ })
@@ -0,0 +1,84 @@
1
+ import { resolve } from 'node:path'
2
+ import { getElapsedMs } from '@internals/utils'
3
+ import { Diagnostics, type GenerationResult, isPerformanceDiagnostic, isProblemDiagnostic, type SerializedDiagnostic } from '@kubb/core'
4
+
5
+ /**
6
+ * One plugin's elapsed time, derived from a `performance` diagnostic.
7
+ */
8
+ export type ReportTiming = {
9
+ plugin: string
10
+ durationMs: number
11
+ }
12
+
13
+ /**
14
+ * The normalized result of generating one config, shared by every reporter. Each reporter renders
15
+ * the same {@link Report} in its own format (the `cli` summary, the `json` document, the `file`
16
+ * log), so they always agree on the numbers. Build it with {@link buildReport}.
17
+ */
18
+ export type Report = {
19
+ /**
20
+ * The config name, or an empty string when it is unnamed.
21
+ */
22
+ name: string
23
+ status: 'success' | 'failed'
24
+ plugins: {
25
+ passed: number
26
+ /**
27
+ * Names of the plugins that failed.
28
+ */
29
+ failed: Array<string>
30
+ total: number
31
+ }
32
+ counts: {
33
+ errors: number
34
+ warnings: number
35
+ infos: number
36
+ }
37
+ filesCreated: number
38
+ /**
39
+ * Wall-clock time spent generating this config, in milliseconds.
40
+ */
41
+ durationMs: number
42
+ /**
43
+ * Absolute output directory the files were written to.
44
+ */
45
+ output: string
46
+ /**
47
+ * Per-plugin durations, slowest first.
48
+ */
49
+ timings: Array<ReportTiming>
50
+ /**
51
+ * The build problems, serialized to their JSON-safe fields plus a `docsUrl`.
52
+ */
53
+ diagnostics: Array<SerializedDiagnostic>
54
+ }
55
+
56
+ /**
57
+ * Builds the normalized {@link Report} for one config from its {@link GenerationResult}. Splits the
58
+ * diagnostics into problems and per-plugin timings (slowest first) and derives the plugin and issue
59
+ * counts, so every reporter renders the same data.
60
+ */
61
+ export function buildReport(result: GenerationResult): Report {
62
+ const { config, diagnostics, filesCreated, status, hrStart } = result
63
+
64
+ const failed = Diagnostics.failedPlugins(diagnostics)
65
+ const total = config.plugins?.length ?? 0
66
+ const counts = Diagnostics.count(diagnostics)
67
+ const problems = diagnostics.filter(isProblemDiagnostic)
68
+ const timings = diagnostics
69
+ .filter(isPerformanceDiagnostic)
70
+ .sort((a, b) => b.duration - a.duration)
71
+ .map((diagnostic) => ({ plugin: diagnostic.plugin, durationMs: diagnostic.duration }))
72
+
73
+ return {
74
+ name: config.name ?? '',
75
+ status,
76
+ plugins: { passed: total - failed.length, failed, total },
77
+ counts,
78
+ filesCreated,
79
+ durationMs: getElapsedMs(hrStart),
80
+ output: resolve(config.root, config.output.path),
81
+ timings,
82
+ diagnostics: problems.map((diagnostic) => Diagnostics.serialize(diagnostic)),
83
+ }
84
+ }
@@ -50,10 +50,10 @@ export async function run({ port, host, configPath, allowWrite, allowAll, versio
50
50
  try {
51
51
  process.loadEnvFile()
52
52
  } catch {
53
- // .env file may not exist; ignore
53
+ // .env file may not exist. Ignore
54
54
  }
55
55
 
56
- // Resolve the @kubb/agent package path createRequire is CJS/ESM compatible (import.meta.resolve is ESM-only)
56
+ // Resolve the @kubb/agent package path, createRequire is CJS/ESM compatible (import.meta.resolve is ESM-only)
57
57
  const require = createRequire(import.meta.url)
58
58
  let agentPkgPath: string
59
59
  try {