@kubb/cli 5.0.0-beta.5 → 5.0.0-beta.50

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 (146) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +171 -51
  3. package/dist/constants-BQ8LZB1P.js +23 -0
  4. package/dist/constants-BQ8LZB1P.js.map +1 -0
  5. package/dist/constants-MzEjK668.cjs +40 -0
  6. package/dist/constants-MzEjK668.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-CKnn3tQa.js +83 -0
  16. package/dist/generate-CKnn3tQa.js.map +1 -0
  17. package/dist/generate-Q6GMu4L8.cjs +82 -0
  18. package/dist/generate-Q6GMu4L8.cjs.map +1 -0
  19. package/dist/index.cjs +22 -15
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +22 -15
  23. package/dist/index.js.map +1 -1
  24. package/dist/init-C5iCqBwA.cjs +53 -0
  25. package/dist/init-C5iCqBwA.cjs.map +1 -0
  26. package/dist/init-DyOF6CiQ.js +53 -0
  27. package/dist/init-DyOF6CiQ.js.map +1 -0
  28. package/dist/mcp-09FvxYWS.js +39 -0
  29. package/dist/mcp-09FvxYWS.js.map +1 -0
  30. package/dist/mcp-BN5HQYdK.cjs +39 -0
  31. package/dist/mcp-BN5HQYdK.cjs.map +1 -0
  32. package/dist/package-Bm6VUbtL.js +6 -0
  33. package/dist/package-Bm6VUbtL.js.map +1 -0
  34. package/dist/{package-BmYRU2hz.cjs → package-DOVOGEEY.cjs} +2 -2
  35. package/dist/package-DOVOGEEY.cjs.map +1 -0
  36. package/dist/run-BO7phoNN.js +51 -0
  37. package/dist/run-BO7phoNN.js.map +1 -0
  38. package/dist/{init-C5sZulT6.cjs → run-BtjzfTz3.cjs} +155 -85
  39. package/dist/run-BtjzfTz3.cjs.map +1 -0
  40. package/dist/run-BzaKz_nl.js +32 -0
  41. package/dist/run-BzaKz_nl.js.map +1 -0
  42. package/dist/{init-Cag3082g.js → run-C-omuksC.js} +147 -77
  43. package/dist/run-C-omuksC.js.map +1 -0
  44. package/dist/run-CkTpemme.cjs +52 -0
  45. package/dist/run-CkTpemme.cjs.map +1 -0
  46. package/dist/run-Cl4SrSob.cjs +33 -0
  47. package/dist/run-Cl4SrSob.cjs.map +1 -0
  48. package/dist/run-CrvmI4G2.cjs +1520 -0
  49. package/dist/run-CrvmI4G2.cjs.map +1 -0
  50. package/dist/run-DvZ5i2lT.js +1517 -0
  51. package/dist/run-DvZ5i2lT.js.map +1 -0
  52. package/dist/{shell-DLzN4fRo.js → shell-DsgkfUSW.js} +2 -2
  53. package/dist/{shell-DLzN4fRo.js.map → shell-DsgkfUSW.js.map} +1 -1
  54. package/dist/{shell-475fQKaX.cjs → shell-Lh-vLWwH.cjs} +2 -2
  55. package/dist/{shell-475fQKaX.cjs.map → shell-Lh-vLWwH.cjs.map} +1 -1
  56. package/dist/validate-5atkOC73.js +26 -0
  57. package/dist/validate-5atkOC73.js.map +1 -0
  58. package/dist/validate-C6KPFEex.cjs +26 -0
  59. package/dist/validate-C6KPFEex.cjs.map +1 -0
  60. package/package.json +14 -24
  61. package/src/commands/generate.ts +27 -13
  62. package/src/commands/init.ts +34 -3
  63. package/src/commands/mcp.ts +28 -4
  64. package/src/commands/validate.ts +6 -4
  65. package/src/constants.ts +1 -86
  66. package/src/index.ts +7 -6
  67. package/src/loggers/clackLogger.ts +137 -178
  68. package/src/loggers/plainLogger.ts +49 -102
  69. package/src/loggers/types.ts +6 -1
  70. package/src/loggers/utils.ts +141 -26
  71. package/src/runners/generate/run.ts +406 -0
  72. package/src/runners/generate/utils.ts +228 -0
  73. package/src/runners/init/run.ts +210 -0
  74. package/src/{utils/packageManager.ts → runners/init/utils.ts} +12 -2
  75. package/src/runners/mcp/run.ts +37 -0
  76. package/src/runners/validate/run.ts +63 -0
  77. package/dist/agent-BcUEl9yB.js +0 -56
  78. package/dist/agent-BcUEl9yB.js.map +0 -1
  79. package/dist/agent-CS45W0kL.cjs +0 -122
  80. package/dist/agent-CS45W0kL.cjs.map +0 -1
  81. package/dist/agent-CsMvXeqI.cjs +0 -58
  82. package/dist/agent-CsMvXeqI.cjs.map +0 -1
  83. package/dist/agent-IP0eLV3C.js +0 -118
  84. package/dist/agent-IP0eLV3C.js.map +0 -1
  85. package/dist/constants-B4iBDvCe.cjs +0 -148
  86. package/dist/constants-B4iBDvCe.cjs.map +0 -1
  87. package/dist/constants-DmPrkaz8.js +0 -95
  88. package/dist/constants-DmPrkaz8.js.map +0 -1
  89. package/dist/generate-7td_hs73.cjs +0 -65
  90. package/dist/generate-7td_hs73.cjs.map +0 -1
  91. package/dist/generate-BqeFFQGD.js +0 -66
  92. package/dist/generate-BqeFFQGD.js.map +0 -1
  93. package/dist/generate-CUd1dUY5.cjs +0 -1755
  94. package/dist/generate-CUd1dUY5.cjs.map +0 -1
  95. package/dist/generate-DVmGtwWe.js +0 -1752
  96. package/dist/generate-DVmGtwWe.js.map +0 -1
  97. package/dist/init-C5sZulT6.cjs.map +0 -1
  98. package/dist/init-Cag3082g.js.map +0 -1
  99. package/dist/init-CfAn19gC.js +0 -25
  100. package/dist/init-CfAn19gC.js.map +0 -1
  101. package/dist/init-CfsYoyGe.cjs +0 -25
  102. package/dist/init-CfsYoyGe.cjs.map +0 -1
  103. package/dist/mcp-BfORW-mY.cjs +0 -47
  104. package/dist/mcp-BfORW-mY.cjs.map +0 -1
  105. package/dist/mcp-BtOV6acy.js +0 -16
  106. package/dist/mcp-BtOV6acy.js.map +0 -1
  107. package/dist/mcp-CIbuLGMx.cjs +0 -16
  108. package/dist/mcp-CIbuLGMx.cjs.map +0 -1
  109. package/dist/mcp-DtQ5o0On.js +0 -46
  110. package/dist/mcp-DtQ5o0On.js.map +0 -1
  111. package/dist/package-BmYRU2hz.cjs.map +0 -1
  112. package/dist/package-DBU5ii-k.js +0 -6
  113. package/dist/package-DBU5ii-k.js.map +0 -1
  114. package/dist/telemetry-BU25EoI-.cjs +0 -282
  115. package/dist/telemetry-BU25EoI-.cjs.map +0 -1
  116. package/dist/telemetry-CaNU4-Bf.js +0 -245
  117. package/dist/telemetry-CaNU4-Bf.js.map +0 -1
  118. package/dist/validate-CQqM9siF.js +0 -25
  119. package/dist/validate-CQqM9siF.js.map +0 -1
  120. package/dist/validate-DBXLaLIn.cjs +0 -34
  121. package/dist/validate-DBXLaLIn.cjs.map +0 -1
  122. package/dist/validate-DI23zgmL.js +0 -33
  123. package/dist/validate-DI23zgmL.js.map +0 -1
  124. package/dist/validate-DwX4LzYq.cjs +0 -25
  125. package/dist/validate-DwX4LzYq.cjs.map +0 -1
  126. package/src/commands/agent/start.ts +0 -47
  127. package/src/commands/agent.ts +0 -8
  128. package/src/loggers/fileSystemLogger.ts +0 -138
  129. package/src/loggers/githubActionsLogger.ts +0 -379
  130. package/src/runners/agent.ts +0 -155
  131. package/src/runners/generate.ts +0 -333
  132. package/src/runners/init.ts +0 -296
  133. package/src/runners/mcp.ts +0 -51
  134. package/src/runners/validate.ts +0 -39
  135. package/src/types.ts +0 -11
  136. package/src/utils/Writables.ts +0 -17
  137. package/src/utils/executeHooks.ts +0 -45
  138. package/src/utils/flags.ts +0 -9
  139. package/src/utils/getConfig.ts +0 -10
  140. package/src/utils/getCosmiConfig.ts +0 -75
  141. package/src/utils/getSummary.ts +0 -68
  142. package/src/utils/runHook.ts +0 -91
  143. package/src/utils/telemetry.ts +0 -273
  144. package/src/utils/watcher.ts +0 -19
  145. /package/dist/{chunk-ByKO4r7w.cjs → chunk-Bx3C2hgW.cjs} +0 -0
  146. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
@@ -3,18 +3,20 @@ import { version } from '../../package.json'
3
3
 
4
4
  export const command = defineCommand({
5
5
  name: 'validate',
6
- description: 'Validate a Swagger/OpenAPI file',
6
+ description:
7
+ 'Parse and validate an OpenAPI/Swagger file for structural correctness. Reports schema errors, missing required fields, and malformed references. Use this before running generate to catch spec issues early.',
8
+ examples: ['kubb validate --input ./openapi.yaml', 'kubb validate --input https://petstore3.swagger.io/api/v3/openapi.json'],
7
9
  options: {
8
10
  input: {
9
11
  type: 'string',
10
- description: 'Path to Swagger/OpenAPI file',
12
+ description: 'Path or URL to the OpenAPI/Swagger file to validate',
11
13
  short: 'i',
12
14
  required: true,
13
15
  },
14
16
  },
15
17
  async run({ values }) {
16
- const { runValidate } = await import('../runners/validate.ts')
18
+ const { run } = await import('../runners/validate/run.ts')
17
19
 
18
- await runValidate({ input: values.input, version })
20
+ await run({ input: values.input, version })
19
21
  },
20
22
  })
package/src/constants.ts CHANGED
@@ -1,104 +1,19 @@
1
- /**
2
- * Default filename for the Kubb configuration file.
3
- *
4
- * Used by the `init` command when scaffolding new projects and by the `agent` default config.
5
- */
6
- export const KUBB_CONFIG_FILENAME = 'kubb.config.ts' as const
7
-
8
1
  /**
9
2
  * NPM registry endpoint used to check for @kubb/cli updates.
10
3
  */
11
4
  export const KUBB_NPM_PACKAGE_URL = 'https://registry.npmjs.org/@kubb/cli/latest' as const
12
5
 
13
- /**
14
- * OpenTelemetry ingestion endpoint for anonymous usage telemetry.
15
- */
16
- export const OTLP_ENDPOINT = 'https://otlp.kubb.dev' as const
17
-
18
6
  /**
19
7
  * Horizontal rule rendered above/below the plain-logger generation summary.
20
8
  */
21
9
  export const SUMMARY_SEPARATOR = '─'.repeat(27)
22
10
 
23
- /**
24
- * Maximum number of █ characters in a plugin timing bar.
25
- */
26
- export const SUMMARY_MAX_BAR_LENGTH = 10 as const
27
-
28
- /**
29
- * Divides elapsed milliseconds into bar-length units (1 block per 100 ms).
30
- */
31
- export const SUMMARY_TIME_SCALE_DIVISOR = 100 as const
32
-
33
11
  /**
34
12
  * Glob pattern for paths the file watcher ignores.
35
13
  */
36
14
  export const WATCHER_IGNORED_PATHS = '**/{.git,node_modules}/**' as const
37
15
 
38
16
  /**
39
- * Flags that short-circuit execution (help/version) no telemetry notice is shown.
17
+ * Flags that short-circuit execution (help/version), no telemetry notice is shown.
40
18
  */
41
19
  export const QUIET_FLAGS = new Set(['--help', '-h', '--version', '-v'] as const)
42
-
43
- export const agentDefaults = {
44
- port: '3000',
45
- host: 'localhost',
46
- configFile: KUBB_CONFIG_FILENAME,
47
- retryTimeout: '30000',
48
- studioUrl: 'https://kubb.studio',
49
- /**
50
- * Relative path from the @kubb/agent package root to the server entry.
51
- */
52
- serverEntryPath: '.output/server/index.mjs',
53
- } as const
54
-
55
- /**
56
- * Default values used during interactive `init` scaffolding.
57
- */
58
- export const initDefaults = {
59
- inputPath: './openapi.yaml',
60
- outputPath: './src/gen',
61
- plugins: ['plugin-ts'],
62
- } as const
63
-
64
- /**
65
- * Maps each plugin value to the default config snippet inserted by `init`.
66
- * The `satisfies` constraint ensures all values remain plain strings while
67
- * `as const` keeps the object deeply immutable.
68
- */
69
- export const pluginDefaultConfigs = {
70
- 'plugin-ts': `pluginTs({
71
- output: { path: 'models' },
72
- })`,
73
- 'plugin-client': `pluginClient({
74
- output: { path: 'clients' },
75
- })`,
76
- 'plugin-react-query': `pluginReactQuery({
77
- output: { path: 'hooks' },
78
- })`,
79
- 'plugin-vue-query': `pluginVueQuery({
80
- output: { path: 'hooks' },
81
- })`,
82
- 'plugin-zod': `pluginZod({
83
- output: { path: 'zod' },
84
- })`,
85
- 'plugin-faker': `pluginFaker({
86
- output: { path: 'mocks' },
87
- })`,
88
- 'plugin-msw': `pluginMsw({
89
- output: { path: 'msw' },
90
- })`,
91
- 'plugin-cypress': `pluginCypress({
92
- output: { path: 'cypress' },
93
- })`,
94
- 'plugin-mcp': `pluginMcp({
95
- output: { path: 'mcp' },
96
- })`,
97
- 'plugin-redoc': `pluginRedoc({
98
- output: { path: 'redoc' },
99
- })`,
100
- } as const satisfies Record<string, string>
101
-
102
- /**
103
- * Color palette used by randomCliColor() for deterministic plugin name coloring.
104
- */
package/src/index.ts CHANGED
@@ -1,24 +1,26 @@
1
1
  import { styleText } from 'node:util'
2
- import { createCLI } from '@internals/utils'
2
+ import { createCLI, isFlag } from '@internals/utils'
3
+ import { Telemetry } from '@kubb/core'
3
4
  import { version } from '../package.json'
4
5
  import { QUIET_FLAGS } from './constants.ts'
5
- import { isFlag } from './utils/flags.ts'
6
- import { isTelemetryDisabled } from './utils/telemetry.ts'
7
6
 
8
7
  const cli = createCLI()
9
8
 
10
9
  function shouldShowTelemetryNotice(argv: Array<string>): boolean {
11
- if (isTelemetryDisabled()) {
10
+ if (Telemetry.isDisabled()) {
12
11
  return false
13
12
  }
13
+
14
14
  // Skip when the user is just asking for help or version info
15
15
  if (argv.some((arg) => isFlag(QUIET_FLAGS, arg))) {
16
16
  return false
17
17
  }
18
+
18
19
  // Skip in non-interactive / scripting contexts
19
20
  if (!process.stdout.isTTY) {
20
21
  return false
21
22
  }
23
+
22
24
  return true
23
25
  }
24
26
 
@@ -32,10 +34,9 @@ export async function run(argv: Array<string> = process.argv): Promise<void> {
32
34
  const { command: generateCommand } = await import('./commands/generate.ts')
33
35
  const { command: validateCommand } = await import('./commands/validate.ts')
34
36
  const { command: mcpCommand } = await import('./commands/mcp.ts')
35
- const { command: agentCommand } = await import('./commands/agent.ts')
36
37
  const { command: initCommand } = await import('./commands/init.ts')
37
38
 
38
- await cli.run([generateCommand, validateCommand, mcpCommand, agentCommand, initCommand], argv, {
39
+ await cli.run([generateCommand, validateCommand, mcpCommand, initCommand], argv, {
39
40
  programName: 'kubb',
40
41
  defaultCommandName: 'generate',
41
42
  version,
@@ -2,12 +2,9 @@ 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 { defineLogger, Diagnostics, type KubbHooks, logLevel as logLevelMap } from '@kubb/core'
7
+ import { buildProgressLine, createProgressCounters, formatCommandWithArgs, formatMessage, recordPluginResult, resetProgressCounters } from './utils.ts'
11
8
 
12
9
  /**
13
10
  * TTY logger with beautiful UI and progress indicators for local development.
@@ -17,34 +14,39 @@ export const clackLogger = defineLogger({
17
14
  install(context, options) {
18
15
  const logLevel = options?.logLevel ?? logLevelMap.info
19
16
  const state = {
20
- totalPlugins: 0,
21
- completedPlugins: 0,
22
- failedPlugins: 0,
23
- totalFiles: 0,
24
- processedFiles: 0,
25
- hrStart: process.hrtime(),
17
+ ...createProgressCounters(),
26
18
  spinner: clack.spinner(),
27
19
  isSpinning: false,
20
+ runningPlugins: new Set<string>(),
28
21
  activeProgress: new Map<string, { interval?: NodeJS.Timeout; progressBar: clack.ProgressResult }>(),
22
+ activeHookLogs: new Map<string, { taskLog: ReturnType<typeof clack.taskLog>; hrStart: [number, number] }>(),
29
23
  }
30
24
 
31
- function reset() {
32
- for (const [_key, active] of state.activeProgress) {
25
+ // Clear every active progress bar's interval, stop it, and drop the map.
26
+ function stopActiveProgress() {
27
+ for (const [, active] of state.activeProgress) {
33
28
  if (active.interval) {
34
29
  clearInterval(active.interval)
35
30
  }
36
31
  active.progressBar?.stop()
37
32
  }
33
+ state.activeProgress.clear()
34
+ }
38
35
 
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()
36
+ function reset() {
37
+ stopActiveProgress()
38
+
39
+ resetProgressCounters(state)
45
40
  state.spinner = clack.spinner()
46
41
  state.isSpinning = false
47
- state.activeProgress.clear()
42
+ state.runningPlugins.clear()
43
+ state.activeHookLogs.clear()
44
+ }
45
+
46
+ // Label for the shared plugin bar, listing the plugins currently generating.
47
+ function pluginProgressText(): string {
48
+ const running = [...state.runningPlugins].map((name) => styleText('bold', name))
49
+ return getMessage(running.length > 0 ? `Generating ${running.join(', ')}` : 'Generating plugins')
48
50
  }
49
51
 
50
52
  function showProgressStep() {
@@ -62,12 +64,25 @@ export const clackLogger = defineLogger({
62
64
  return formatMessage(message, logLevel)
63
65
  }
64
66
 
67
+ // Registers a handler that prints a fixed step message, skipped at silent level.
68
+ function onStep<E extends keyof KubbHooks>(event: E, message: string): void {
69
+ context.on(event, () => {
70
+ if (logLevel <= logLevelMap.silent) {
71
+ return
72
+ }
73
+ clack.log.step(getMessage(message))
74
+ })
75
+ }
76
+
65
77
  function startSpinner(text?: string) {
66
78
  state.spinner.start(text)
67
79
  state.isSpinning = true
68
80
  }
69
81
 
70
82
  function stopSpinner(text?: string) {
83
+ if (!state.isSpinning) {
84
+ return
85
+ }
71
86
  state.spinner.stop(text)
72
87
  state.isSpinning = false
73
88
  }
@@ -77,13 +92,13 @@ export const clackLogger = defineLogger({
77
92
  return
78
93
  }
79
94
 
80
- const text = getMessage([styleText('blue', 'ℹ'), message, styleText('dim', info)].join(' '))
95
+ const text = getMessage([styleText('blue', 'ℹ'), message, info ? styleText('dim', info) : undefined].filter(Boolean).join(' '))
81
96
 
82
97
  if (state.isSpinning) {
83
98
  state.spinner.message(text)
84
- } else {
85
- clack.log.info(text)
99
+ return
86
100
  }
101
+ clack.log.info(text)
87
102
  })
88
103
 
89
104
  context.on('kubb:success', ({ message, info = '' }) => {
@@ -95,9 +110,9 @@ export const clackLogger = defineLogger({
95
110
 
96
111
  if (state.isSpinning) {
97
112
  stopSpinner(text)
98
- } else {
99
- clack.log.success(text)
113
+ return
100
114
  }
115
+ clack.log.success(text)
101
116
  })
102
117
 
103
118
  context.on('kubb:warn', ({ message, info }) => {
@@ -119,12 +134,12 @@ export const clackLogger = defineLogger({
119
134
 
120
135
  if (state.isSpinning) {
121
136
  stopSpinner(getMessage(text))
122
- } else {
123
- clack.log.error(getMessage(text))
137
+ return
124
138
  }
139
+ clack.log.error(getMessage(text))
125
140
 
126
- // Show stack trace in debug mode (first 3 frames)
127
- if (logLevel >= logLevelMap.debug && error.stack) {
141
+ // Show stack trace in verbose mode (first 3 frames)
142
+ if (logLevel >= logLevelMap.verbose && error.stack) {
128
143
  const frames = error.stack.split('\n').slice(1, 4)
129
144
  for (const frame of frames) {
130
145
  clack.log.message(getMessage(styleText('dim', frame.trim())))
@@ -141,14 +156,21 @@ export const clackLogger = defineLogger({
141
156
  }
142
157
  })
143
158
 
144
- context.on('kubb:version:new', ({ currentVersion, latestVersion }) => {
145
- if (logLevel <= logLevelMap.silent) {
159
+ context.on('kubb:diagnostic', ({ diagnostic }) => {
160
+ // Silent still surfaces errors so failures stay visible. It drops warnings and info.
161
+ if (logLevel <= logLevelMap.silent && diagnostic.severity !== 'error') {
146
162
  return
147
163
  }
148
164
 
149
- try {
165
+ stopSpinner()
166
+
167
+ // Stop any lingering progress UI so the multi-line block renders cleanly.
168
+ stopActiveProgress()
169
+
170
+ // The version-update notice keeps its own framed box instead of the diagnostic gutter.
171
+ if (Diagnostics.isUpdate(diagnostic)) {
150
172
  clack.box(
151
- `\`v${currentVersion}\` → \`v${latestVersion}\`
173
+ `\`v${diagnostic.currentVersion}\` → \`v${diagnostic.latestVersion}\`
152
174
  Run \`npm install -g @kubb/cli\` to update`,
153
175
  'Update available for `Kubb`',
154
176
  {
@@ -160,14 +182,19 @@ Run \`npm install -g @kubb/cli\` to update`,
160
182
  titleAlign: 'center',
161
183
  },
162
184
  )
163
- } catch {
164
- console.log(`Update available for Kubb: v${currentVersion} → v${latestVersion}`)
165
- console.log('Run `npm install -g @kubb/cli` to update')
185
+
186
+ return
166
187
  }
188
+
189
+ // Hand the severity glyph to clack as the gutter `symbol`, then let it draw the
190
+ // bar on each detail line via the default `secondarySymbol`. The headline and
191
+ // details carry their own colors, so clack only owns the gutter.
192
+ const { symbol, headline, details } = Diagnostics.format(diagnostic)
193
+ clack.log.message([headline, ...details], { symbol })
167
194
  })
168
195
 
169
196
  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`)
197
+ console.log(`\n${getIntro({ title: 'The meta framework for code generation', description: 'Ready to start', version, areEyesOpen: true })}\n`)
171
198
 
172
199
  reset()
173
200
  })
@@ -199,11 +226,17 @@ Run \`npm install -g @kubb/cli\` to update`,
199
226
  // Initialize progress tracking for this generation
200
227
  state.totalPlugins = config.plugins?.length ?? 0
201
228
 
229
+ if (logLevel <= logLevelMap.silent) {
230
+ return
231
+ }
232
+
202
233
  const text = getMessage(['Generation started', config.name ? `for ${styleText('dim', config.name)}` : undefined].filter(Boolean).join(' '))
203
234
 
204
235
  clack.intro(text)
205
236
  })
206
237
 
238
+ // Plugins run concurrently, so they share a single progress bar. A bar per plugin
239
+ // would make clack render them side by side and pile up keypress listeners.
207
240
  context.on('kubb:plugin:start', ({ plugin }) => {
208
241
  if (logLevel <= logLevelMap.silent) {
209
242
  return
@@ -211,50 +244,44 @@ Run \`npm install -g @kubb/cli\` to update`,
211
244
 
212
245
  stopSpinner()
213
246
 
247
+ state.runningPlugins.add(plugin.name)
248
+
249
+ const active = state.activeProgress.get('plugins')
250
+ if (active) {
251
+ active.progressBar.advance(0, pluginProgressText())
252
+ return
253
+ }
254
+
214
255
  const progressBar = clack.progress({
215
256
  style: 'block',
216
- max: 100,
257
+ max: Math.max(state.totalPlugins, 1),
217
258
  size: 30,
218
259
  })
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 })
260
+ progressBar.start(pluginProgressText())
261
+ // Catch up to plugins already finished before this bar opened.
262
+ progressBar.advance(state.completedPlugins + state.failedPlugins, pluginProgressText())
263
+ state.activeProgress.set('plugins', { progressBar })
227
264
  })
228
265
 
229
- context.on('kubb:plugin:end', ({ plugin, duration, success }) => {
266
+ context.on('kubb:plugin:end', ({ plugin, success }) => {
230
267
  stopSpinner()
231
268
 
232
- const active = state.activeProgress.get(plugin.name)
269
+ const active = state.activeProgress.get('plugins')
233
270
 
234
271
  if (!active || logLevel === logLevelMap.silent) {
235
272
  return
236
273
  }
237
274
 
238
- clearInterval(active.interval)
275
+ state.runningPlugins.delete(plugin.name)
276
+ recordPluginResult(state, success)
277
+ active.progressBar.advance(1, pluginProgressText())
239
278
 
240
- if (success) {
241
- state.completedPlugins++
242
- } else {
243
- state.failedPlugins++
279
+ // Close the bar once nothing is generating, then print the progress step.
280
+ if (state.runningPlugins.size === 0) {
281
+ active.progressBar.stop(getMessage('Plugins generated'))
282
+ state.activeProgress.delete('plugins')
283
+ showProgressStep()
244
284
  }
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
285
  })
259
286
 
260
287
  context.on('kubb:files:processing:start', ({ files }) => {
@@ -279,23 +306,20 @@ Run \`npm install -g @kubb/cli\` to update`,
279
306
  state.activeProgress.set('files', { progressBar })
280
307
  })
281
308
 
282
- context.on('kubb:file:processing:update', ({ file, config }) => {
309
+ context.on('kubb:files:processing:update', ({ files }) => {
283
310
  if (logLevel <= logLevelMap.silent) {
284
311
  return
285
312
  }
286
313
 
287
314
  stopSpinner()
288
315
 
289
- state.processedFiles++
290
-
291
- const text = `Writing ${relative(config.root, file.path)}`
292
316
  const active = state.activeProgress.get('files')
293
-
294
- if (!active) {
295
- return
317
+ for (const { file, config } of files) {
318
+ state.processedFiles++
319
+ if (active) {
320
+ active.progressBar.advance(undefined, `Writing ${relative(config.root, file.path)}`)
321
+ }
296
322
  }
297
-
298
- active.progressBar.advance(undefined, text)
299
323
  })
300
324
  context.on('kubb:files:processing:end', () => {
301
325
  if (logLevel <= logLevelMap.silent) {
@@ -319,135 +343,70 @@ Run \`npm install -g @kubb/cli\` to update`,
319
343
  })
320
344
 
321
345
  context.on('kubb:generation:end', ({ config }) => {
346
+ stopSpinner()
347
+
322
348
  const text = getMessage(config.name ? `Generation completed for ${styleText('dim', config.name)}` : 'Generation completed')
323
349
 
324
350
  clack.outro(text)
325
351
  })
326
352
 
327
- context.on('kubb:format:start', () => {
328
- if (logLevel <= logLevelMap.silent) {
329
- return
330
- }
353
+ onStep('kubb:format:start', 'Formatting')
354
+ onStep('kubb:lint:start', 'Linting')
355
+ onStep('kubb:hooks:start', 'Running hooks')
331
356
 
332
- const text = getMessage('Format started')
333
-
334
- clack.intro(text)
335
- })
336
-
337
- context.on('kubb:format:end', () => {
338
- if (logLevel <= logLevelMap.silent) {
357
+ context.on('kubb:hook:start', ({ id, command, args }) => {
358
+ if (logLevel <= logLevelMap.silent || !id) {
339
359
  return
340
360
  }
341
361
 
342
- const text = getMessage('Format completed')
343
-
344
- clack.outro(text)
345
- })
346
-
347
- context.on('kubb:lint:start', () => {
348
- if (logLevel <= logLevelMap.silent) {
349
- return
350
- }
351
-
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
- }
362
+ stopSpinner()
361
363
 
362
- const text = getMessage('Lint completed')
364
+ const commandWithArgs = formatCommandWithArgs(command, args)
365
+ const title = getMessage(`Running ${styleText('dim', commandWithArgs)}`)
366
+ const taskLog = clack.taskLog({ title })
363
367
 
364
- clack.outro(text)
368
+ state.activeHookLogs.set(id, { taskLog, hrStart: process.hrtime() })
365
369
  })
366
370
 
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`)
371
+ // Registered only when not silent, so its presence is what tells the runner to stream
372
+ // (`kubb:hook:line` listenerCount). At silent level the listener is absent, so no streaming happens.
373
+ if (logLevel > logLevelMap.silent) {
374
+ context.on('kubb:hook:line', ({ id, line }) => {
375
+ const active = state.activeHookLogs.get(id)
376
+ active?.taskLog.message(styleText('dim', line))
377
+ })
378
+ }
370
379
 
371
- // Skip hook execution if no id is provided (e.g., during benchmarks or tests)
380
+ context.on('kubb:hook:end', ({ id, command, args, success, error, stdout, stderr }) => {
372
381
  if (!id) {
373
382
  return
374
383
  }
375
384
 
376
385
  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
- })
386
+ // Even when silent, surface a failed hook's captured output.
387
+ if (!success) {
388
+ if (stdout) console.log(stdout)
389
+ if (stderr) console.error(stderr)
390
+ }
388
391
  return
389
392
  }
390
393
 
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) {
394
+ const active = state.activeHookLogs.get(id)
395
+ if (!active) {
416
396
  return
417
397
  }
398
+ state.activeHookLogs.delete(id)
418
399
 
419
400
  const commandWithArgs = formatCommandWithArgs(command, args)
420
- const text = getMessage(`Hook ${styleText('dim', commandWithArgs)} successfully executed`)
401
+ const duration = formatMsWithColor(getElapsedMs(active.hrStart))
421
402
 
422
- clack.outro(text)
423
- })
424
-
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'))
403
+ if (success) {
404
+ active.taskLog.success(getMessage(`${styleText('dim', commandWithArgs)} completed in ${duration}`))
405
+ } else {
406
+ // The hook's output already reached the taskLog live via `kubb:hook:line`, so `showLog`
407
+ // replays it here; `kubb:hook:end` carries no captured output on the streaming path.
408
+ const reason = error?.message ? ` (${error.message})` : ''
409
+ active.taskLog.error(getMessage(`${styleText('dim', commandWithArgs)} failed${reason}`), { showLog: true })
451
410
  }
452
411
  })
453
412