@kubb/cli 5.0.0-beta.35 → 5.0.0-beta.37

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-CY4CGQxI.js → agent-B8oJFhcN.js} +4 -4
  3. package/dist/{agent-CY4CGQxI.js.map → agent-B8oJFhcN.js.map} +1 -1
  4. package/dist/{agent-CX220hJL.cjs → agent-DtuTV_Qk.cjs} +4 -4
  5. package/dist/{agent-CX220hJL.cjs.map → agent-DtuTV_Qk.cjs.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-DYPz-3vT.cjs → generate-BvaMqrBk.cjs} +20 -14
  11. package/dist/generate-BvaMqrBk.cjs.map +1 -0
  12. package/dist/{generate-DuzUhvOd.js → generate-CzTjeiji.js} +20 -14
  13. package/dist/generate-CzTjeiji.js.map +1 -0
  14. package/dist/index.cjs +8 -8
  15. package/dist/index.js +8 -8
  16. package/dist/{init-D6916j9S.cjs → init-C59u3T68.cjs} +2 -2
  17. package/dist/{init-D6916j9S.cjs.map → init-C59u3T68.cjs.map} +1 -1
  18. package/dist/{init-lIh2Ooks.js → init-CaMeuE1-.js} +2 -2
  19. package/dist/{init-lIh2Ooks.js.map → init-CaMeuE1-.js.map} +1 -1
  20. package/dist/{mcp-tiR3udeu.js → mcp-Ca3ZcpKB.js} +3 -3
  21. package/dist/{mcp-tiR3udeu.js.map → mcp-Ca3ZcpKB.js.map} +1 -1
  22. package/dist/{mcp-Cl02BXV5.cjs → mcp-D4NMV9lk.cjs} +3 -3
  23. package/dist/{mcp-Cl02BXV5.cjs.map → mcp-D4NMV9lk.cjs.map} +1 -1
  24. package/dist/{package-B121qOXj.cjs → package-DQFf9DB2.cjs} +2 -2
  25. package/dist/package-DQFf9DB2.cjs.map +1 -0
  26. package/dist/package-DUwUSFeL.js +6 -0
  27. package/dist/package-DUwUSFeL.js.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-CCgNPz0F.cjs → run-BFv6avA_.cjs} +3 -3
  31. package/dist/{run-CCgNPz0F.cjs.map → run-BFv6avA_.cjs.map} +1 -1
  32. package/dist/{run-DpDKN_rb.cjs → run-BQZyg7If.cjs} +2 -2
  33. package/dist/{run-DpDKN_rb.cjs.map → run-BQZyg7If.cjs.map} +1 -1
  34. package/dist/{run-CPimpDgO.js → run-BvXxelGR.js} +2 -2
  35. package/dist/{run-CPimpDgO.js.map → run-BvXxelGR.js.map} +1 -1
  36. package/dist/{run-Lnupy7qb.cjs → run-Bz9IFMWg.cjs} +2 -2
  37. package/dist/{run-Lnupy7qb.cjs.map → run-Bz9IFMWg.cjs.map} +1 -1
  38. package/dist/{run-tnqS6GZS.cjs → run-CK8Cvq6n.cjs} +485 -310
  39. package/dist/run-CK8Cvq6n.cjs.map +1 -0
  40. package/dist/run-C_NMctua.cjs.map +1 -1
  41. package/dist/{run-zuPIKTwa.js → run-Ca2h07rN.js} +551 -376
  42. package/dist/run-Ca2h07rN.js.map +1 -0
  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-Cjvn6prf.js → validate-BEEerg2-.js} +3 -3
  51. package/dist/{validate-Cjvn6prf.js.map → validate-BEEerg2-.js.map} +1 -1
  52. package/dist/{validate-3ROToMMX.cjs → validate-B_wfDSHQ.cjs} +3 -3
  53. package/dist/{validate-3ROToMMX.cjs.map → validate-B_wfDSHQ.cjs.map} +1 -1
  54. package/package.json +7 -7
  55. package/src/commands/generate.ts +17 -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 +43 -96
  63. package/src/reporters/cliReporter.ts +89 -0
  64. package/src/reporters/fileReporter.ts +103 -0
  65. package/src/reporters/jsonReporter.ts +15 -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-DYPz-3vT.cjs.map +0 -1
  73. package/dist/generate-DuzUhvOd.js.map +0 -1
  74. package/dist/package-B0p9bKKV.js +0 -6
  75. package/dist/package-B0p9bKKV.js.map +0 -1
  76. package/dist/package-B121qOXj.cjs.map +0 -1
  77. package/dist/run-tnqS6GZS.cjs.map +0 -1
  78. package/dist/run-zuPIKTwa.js.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,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,52 @@ 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 })
239
-
240
- if (logLevel >= logLevelMap.debug) {
241
- await fileSystemLogger.install(context, { logLevel })
242
- }
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>
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
+ })
272
237
  }
273
238
 
274
239
  /**
275
- * Builds the generation summary lines rendered in the end-of-run box.
276
- * Returns an array of styled strings, one per summary row.
240
+ * Installs the live logger (the TUI view) and the selected reporters (the output), returning the
241
+ * terminal logger's hook sink when one was installed. Loggers and reporters are independent: the
242
+ * `cli` selection activates the env logger plus the {@link cliReporter} summary.
243
+ *
244
+ * The `json` reporter owns stdout, so the terminal logger and `cli` summary are suppressed whenever
245
+ * `json` is selected, even if `cli` is also listed.
277
246
  */
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:',
247
+ export async function setupReporters(
248
+ context: LoggerContext,
249
+ { logLevel, reporters }: LoggerOptions & { reporters: ReadonlyArray<ReporterName> },
250
+ ): Promise<HookSinkFactory | null> {
251
+ const unique = new Set<ReporterName>(reporters.length ? reporters : ['cli'])
252
+ const hasJson = unique.has('json')
253
+ const ctx: ReporterContext = { logLevel }
254
+
255
+ let makeSink: HookSinkFactory | null = null
256
+
257
+ if (unique.has('cli') && !hasJson) {
258
+ const type = detectLogger()
259
+ const logger = logMapper[type]
260
+ if (!logger) {
261
+ throw new Error(`Unknown adapter type: ${type}`)
262
+ }
263
+ const sink = await logger.install(context, { logLevel })
264
+ makeSink = typeof sink === 'function' ? sink : null
265
+ installReporter(context, cliReporter, ctx)
301
266
  }
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
267
 
307
- if (meta.pluginsFailed) {
308
- summaryLines.push(`${labels.failed.padEnd(maxLength + 2)} ${meta.pluginsFailed}`)
268
+ if (hasJson) {
269
+ installReporter(context, jsonReporter, ctx)
309
270
  }
310
271
 
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
- })
272
+ if (unique.has('file')) {
273
+ installReporter(context, fileReporter, ctx)
325
274
  }
326
275
 
327
- summaryLines.push(`${labels.output.padEnd(maxLength + 2)} ${meta.output}`)
328
-
329
- return summaryLines
276
+ return makeSink
330
277
  }
@@ -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,15 @@
1
+ import process from 'node:process'
2
+ import { createReporter } from '@kubb/core'
3
+ import { buildReport } from './report.ts'
4
+
5
+ /**
6
+ * The `json` reporter. Writes the {@link Report} for each config to stdout as JSON, for CI tooling.
7
+ * The terminal reporter is suppressed while this is active so stdout stays valid JSON.
8
+ */
9
+ export const jsonReporter = createReporter({
10
+ name: 'json',
11
+ report(result) {
12
+ const report = buildReport(result)
13
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
14
+ },
15
+ })
@@ -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 {