@kubb/cli 4.32.4 → 4.33.1

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 (133) hide show
  1. package/dist/agent-Bd1QdPVV.cjs +91 -0
  2. package/dist/agent-Bd1QdPVV.cjs.map +1 -0
  3. package/dist/agent-D83d9Pud.cjs +60 -0
  4. package/dist/agent-D83d9Pud.cjs.map +1 -0
  5. package/dist/agent-DgKQXSmR.js +57 -0
  6. package/dist/agent-DgKQXSmR.js.map +1 -0
  7. package/dist/agent-u_Ehwz6r.js +87 -0
  8. package/dist/agent-u_Ehwz6r.js.map +1 -0
  9. package/dist/constants-BTUap0zs.cjs +108 -0
  10. package/dist/constants-BTUap0zs.cjs.map +1 -0
  11. package/dist/constants-CM3dJzjK.js +67 -0
  12. package/dist/constants-CM3dJzjK.js.map +1 -0
  13. package/dist/define--M_JMcDC.js +25 -0
  14. package/dist/define--M_JMcDC.js.map +1 -0
  15. package/dist/define-D6Kfm7-Z.cjs +36 -0
  16. package/dist/define-D6Kfm7-Z.cjs.map +1 -0
  17. package/dist/errors-6mF_WKxg.js +27 -0
  18. package/dist/errors-6mF_WKxg.js.map +1 -0
  19. package/dist/errors-DBW0N9w4.cjs +44 -0
  20. package/dist/errors-DBW0N9w4.cjs.map +1 -0
  21. package/dist/generate-Bn8n4w1O.cjs +65 -0
  22. package/dist/generate-Bn8n4w1O.cjs.map +1 -0
  23. package/dist/{generate-CpWtSc45.js → generate-CAsV9wSx.js} +656 -689
  24. package/dist/generate-CAsV9wSx.js.map +1 -0
  25. package/dist/generate-D-59YK0L.js +66 -0
  26. package/dist/generate-D-59YK0L.js.map +1 -0
  27. package/dist/{generate-COj0aMS6.cjs → generate-JC65igQh.cjs} +662 -694
  28. package/dist/generate-JC65igQh.cjs.map +1 -0
  29. package/dist/index.cjs +226 -35
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +226 -35
  33. package/dist/index.js.map +1 -1
  34. package/dist/init-C-InrmSY.js +302 -0
  35. package/dist/init-C-InrmSY.js.map +1 -0
  36. package/dist/init-CXP8OfMe.js +25 -0
  37. package/dist/init-CXP8OfMe.js.map +1 -0
  38. package/dist/init-CbeE-L-0.cjs +25 -0
  39. package/dist/init-CbeE-L-0.cjs.map +1 -0
  40. package/dist/init-hmolV6B4.cjs +306 -0
  41. package/dist/init-hmolV6B4.cjs.map +1 -0
  42. package/dist/jiti-Cd3S0xwr.cjs +16 -0
  43. package/dist/jiti-Cd3S0xwr.cjs.map +1 -0
  44. package/dist/jiti-e08mD2Ph.js +11 -0
  45. package/dist/jiti-e08mD2Ph.js.map +1 -0
  46. package/dist/mcp-BDxg2oJm.cjs +16 -0
  47. package/dist/mcp-BDxg2oJm.cjs.map +1 -0
  48. package/dist/mcp-ChHFPRzD.cjs +42 -0
  49. package/dist/mcp-ChHFPRzD.cjs.map +1 -0
  50. package/dist/mcp-D2SHEg_d.js +41 -0
  51. package/dist/mcp-D2SHEg_d.js.map +1 -0
  52. package/dist/mcp-MSoE4vNA.js +16 -0
  53. package/dist/mcp-MSoE4vNA.js.map +1 -0
  54. package/dist/{package-aNQWvWbS.cjs → package-CUVyeIbt.cjs} +2 -2
  55. package/dist/package-CUVyeIbt.cjs.map +1 -0
  56. package/dist/package-Cbd8OC6q.js +6 -0
  57. package/dist/package-Cbd8OC6q.js.map +1 -0
  58. package/dist/shell-7HPrTCJ5.cjs +57 -0
  59. package/dist/shell-7HPrTCJ5.cjs.map +1 -0
  60. package/dist/shell-DqqWsHCD.js +46 -0
  61. package/dist/shell-DqqWsHCD.js.map +1 -0
  62. package/dist/{telemetry-BDSSqUiG.cjs → telemetry-Cn9X1I5B.cjs} +79 -9
  63. package/dist/telemetry-Cn9X1I5B.cjs.map +1 -0
  64. package/dist/{telemetry-DYWvlxqs.js → telemetry-DxiR7clS.js} +63 -11
  65. package/dist/telemetry-DxiR7clS.js.map +1 -0
  66. package/dist/validate-BG8A3aQS.cjs +25 -0
  67. package/dist/validate-BG8A3aQS.cjs.map +1 -0
  68. package/dist/validate-BZ1UFkwA.js +25 -0
  69. package/dist/validate-BZ1UFkwA.js.map +1 -0
  70. package/dist/validate-Bbrn3Q-A.cjs +42 -0
  71. package/dist/validate-Bbrn3Q-A.cjs.map +1 -0
  72. package/dist/validate-l8vLmwKA.js +41 -0
  73. package/dist/validate-l8vLmwKA.js.map +1 -0
  74. package/package.json +6 -6
  75. package/src/commands/agent/start.ts +27 -136
  76. package/src/commands/agent.ts +6 -25
  77. package/src/commands/generate.ts +26 -158
  78. package/src/commands/init.ts +9 -360
  79. package/src/commands/mcp.ts +7 -52
  80. package/src/commands/validate.ts +9 -60
  81. package/src/constants.ts +76 -0
  82. package/src/index.ts +36 -42
  83. package/src/loggers/clackLogger.ts +65 -165
  84. package/src/loggers/fileSystemLogger.ts +2 -14
  85. package/src/loggers/githubActionsLogger.ts +58 -125
  86. package/src/loggers/plainLogger.ts +44 -92
  87. package/src/loggers/utils.ts +67 -4
  88. package/src/runners/agent.ts +100 -0
  89. package/src/runners/generate.ts +223 -102
  90. package/src/runners/init.ts +323 -0
  91. package/src/runners/mcp.ts +32 -0
  92. package/src/runners/validate.ts +35 -0
  93. package/src/utils/Writables.ts +2 -2
  94. package/src/utils/executeHooks.ts +20 -8
  95. package/src/utils/getCosmiConfig.ts +10 -11
  96. package/src/utils/getIntro.ts +1 -81
  97. package/src/utils/getSummary.ts +12 -17
  98. package/src/utils/jiti.ts +9 -0
  99. package/src/utils/packageManager.ts +4 -4
  100. package/src/utils/runHook.ts +75 -0
  101. package/src/utils/telemetry.ts +8 -26
  102. package/src/utils/watcher.ts +2 -4
  103. package/dist/agent-6COck3B9.cjs +0 -20
  104. package/dist/agent-6COck3B9.cjs.map +0 -1
  105. package/dist/agent-DMm6c5Vg.js +0 -20
  106. package/dist/agent-DMm6c5Vg.js.map +0 -1
  107. package/dist/generate-COj0aMS6.cjs.map +0 -1
  108. package/dist/generate-CpWtSc45.js.map +0 -1
  109. package/dist/init-Bdn3_qir.js +0 -304
  110. package/dist/init-Bdn3_qir.js.map +0 -1
  111. package/dist/init-CFW2kWY8.cjs +0 -308
  112. package/dist/init-CFW2kWY8.cjs.map +0 -1
  113. package/dist/mcp-DkwtARfo.cjs +0 -57
  114. package/dist/mcp-DkwtARfo.cjs.map +0 -1
  115. package/dist/mcp-DrH93Vq4.js +0 -57
  116. package/dist/mcp-DrH93Vq4.js.map +0 -1
  117. package/dist/package-BnJbGmLm.js +0 -6
  118. package/dist/package-BnJbGmLm.js.map +0 -1
  119. package/dist/package-aNQWvWbS.cjs.map +0 -1
  120. package/dist/start-CqTUu14n.js +0 -131
  121. package/dist/start-CqTUu14n.js.map +0 -1
  122. package/dist/start-D-rsIJGo.cjs +0 -134
  123. package/dist/start-D-rsIJGo.cjs.map +0 -1
  124. package/dist/telemetry-BDSSqUiG.cjs.map +0 -1
  125. package/dist/telemetry-DYWvlxqs.js.map +0 -1
  126. package/dist/validate-BlV8L8gC.js +0 -66
  127. package/dist/validate-BlV8L8gC.js.map +0 -1
  128. package/dist/validate-COhZUXF8.cjs +0 -66
  129. package/dist/validate-COhZUXF8.cjs.map +0 -1
  130. package/src/loggers/envDetection.ts +0 -28
  131. package/src/loggers/index.ts +0 -5
  132. package/src/utils/formatMsWithColor.ts +0 -22
  133. package/src/utils/randomColor.ts +0 -23
@@ -0,0 +1,323 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { styleText } from 'node:util'
5
+ import * as clack from '@clack/prompts'
6
+ import type { PackageManagerInfo } from '@internals/utils'
7
+ import { detectPackageManager } from '@internals/utils'
8
+ import { initDefaults, pluginDefaultConfigs } from '../constants.ts'
9
+ import { hasPackageJson, initPackageJson, installPackages } from '../utils/packageManager.ts'
10
+
11
+ type PluginOption = {
12
+ value: string
13
+ label: string
14
+ hint?: string
15
+ packageName: string
16
+ importName: string
17
+ category: 'core' | 'typescript' | 'query' | 'validation' | 'testing' | 'mocking' | 'docs'
18
+ }
19
+
20
+ const availablePlugins: PluginOption[] = [
21
+ {
22
+ value: 'plugin-oas',
23
+ label: 'OpenAPI Parser',
24
+ hint: 'Required',
25
+ packageName: '@kubb/plugin-oas',
26
+ importName: 'pluginOas',
27
+ category: 'core',
28
+ },
29
+ {
30
+ value: 'plugin-ts',
31
+ label: 'TypeScript',
32
+ hint: 'Recommended',
33
+ packageName: '@kubb/plugin-ts',
34
+ importName: 'pluginTs',
35
+ category: 'typescript',
36
+ },
37
+ {
38
+ value: 'plugin-client',
39
+ label: 'Client (Fetch/Axios)',
40
+ packageName: '@kubb/plugin-client',
41
+ importName: 'pluginClient',
42
+ category: 'typescript',
43
+ },
44
+ {
45
+ value: 'plugin-react-query',
46
+ label: 'React Query / TanStack Query',
47
+ packageName: '@kubb/plugin-react-query',
48
+ importName: 'pluginReactQuery',
49
+ category: 'query',
50
+ },
51
+ {
52
+ value: 'plugin-solid-query',
53
+ label: 'Solid Query',
54
+ packageName: '@kubb/plugin-solid-query',
55
+ importName: 'pluginSolidQuery',
56
+ category: 'query',
57
+ },
58
+ {
59
+ value: 'plugin-svelte-query',
60
+ label: 'Svelte Query',
61
+ packageName: '@kubb/plugin-svelte-query',
62
+ importName: 'pluginSvelteQuery',
63
+ category: 'query',
64
+ },
65
+ {
66
+ value: 'plugin-vue-query',
67
+ label: 'Vue Query',
68
+ packageName: '@kubb/plugin-vue-query',
69
+ importName: 'pluginVueQuery',
70
+ category: 'query',
71
+ },
72
+ {
73
+ value: 'plugin-swr',
74
+ label: 'SWR',
75
+ packageName: '@kubb/plugin-swr',
76
+ importName: 'pluginSwr',
77
+ category: 'query',
78
+ },
79
+ {
80
+ value: 'plugin-zod',
81
+ label: 'Zod Schemas',
82
+ packageName: '@kubb/plugin-zod',
83
+ importName: 'pluginZod',
84
+ category: 'validation',
85
+ },
86
+ {
87
+ value: 'plugin-faker',
88
+ label: 'Faker.js Mocks',
89
+ packageName: '@kubb/plugin-faker',
90
+ importName: 'pluginFaker',
91
+ category: 'mocking',
92
+ },
93
+ {
94
+ value: 'plugin-msw',
95
+ label: 'MSW Handlers',
96
+ packageName: '@kubb/plugin-msw',
97
+ importName: 'pluginMsw',
98
+ category: 'mocking',
99
+ },
100
+ {
101
+ value: 'plugin-cypress',
102
+ label: 'Cypress Tests',
103
+ packageName: '@kubb/plugin-cypress',
104
+ importName: 'pluginCypress',
105
+ category: 'testing',
106
+ },
107
+ {
108
+ value: 'plugin-redoc',
109
+ label: 'ReDoc Documentation',
110
+ packageName: '@kubb/plugin-redoc',
111
+ importName: 'pluginRedoc',
112
+ category: 'docs',
113
+ },
114
+ ]
115
+
116
+ function generateConfigFile(selectedPlugins: PluginOption[], inputPath: string, outputPath: string): string {
117
+ const imports = selectedPlugins.map((plugin) => `import { ${plugin.importName} } from '${plugin.packageName}'`).join('\n')
118
+
119
+ const pluginConfigs = selectedPlugins
120
+ .map((plugin) => {
121
+ const config = (pluginDefaultConfigs as Record<string, string>)[plugin.value] ?? `${plugin.importName}()`
122
+ return ` ${config},`
123
+ })
124
+ .join('\n')
125
+
126
+ return `import { defineConfig } from '@kubb/core'
127
+ ${imports}
128
+
129
+ export default defineConfig({
130
+ root: '.',
131
+ input: {
132
+ path: '${inputPath}',
133
+ },
134
+ output: {
135
+ path: '${outputPath}',
136
+ clean: true,
137
+ },
138
+ plugins: [
139
+ ${pluginConfigs}
140
+ ],
141
+ })
142
+ `
143
+ }
144
+
145
+ function cancelAndExit(message = 'Operation cancelled.'): never {
146
+ clack.cancel(message)
147
+ process.exit(0)
148
+ }
149
+
150
+ type InitOptions = {
151
+ yes: boolean
152
+ version: string
153
+ }
154
+
155
+ export async function runInit({ yes, version }: InitOptions): Promise<void> {
156
+ const cwd = process.cwd()
157
+
158
+ clack.intro(styleText('bgCyan', styleText('black', ' Kubb Init ')))
159
+
160
+ try {
161
+ // Check/create package.json
162
+ let packageManager: PackageManagerInfo
163
+ if (!hasPackageJson(cwd)) {
164
+ if (!yes) {
165
+ const shouldInit = await clack.confirm({
166
+ message: 'No package.json found. Would you like to create one?',
167
+ initialValue: true,
168
+ })
169
+
170
+ if (clack.isCancel(shouldInit) || !shouldInit) {
171
+ cancelAndExit()
172
+ }
173
+ }
174
+
175
+ packageManager = detectPackageManager(cwd)
176
+
177
+ const spinner = clack.spinner()
178
+ spinner.start(`Initializing package.json with ${packageManager.name}`)
179
+
180
+ await initPackageJson(cwd, packageManager)
181
+
182
+ spinner.stop(`Created package.json with ${packageManager.name}`)
183
+ } else {
184
+ packageManager = detectPackageManager(cwd)
185
+ clack.log.info(`Detected package manager: ${styleText('cyan', packageManager.name)}`)
186
+ }
187
+
188
+ // Prompt for OpenAPI spec path
189
+ let inputPath: string
190
+ if (yes) {
191
+ inputPath = initDefaults.inputPath
192
+ clack.log.info(`Using input path: ${styleText('cyan', inputPath)}`)
193
+ } else {
194
+ const inputPathResult = await clack.text({
195
+ message: 'Where is your OpenAPI specification located?',
196
+ placeholder: initDefaults.inputPath,
197
+ defaultValue: initDefaults.inputPath,
198
+ validate: (value) => {
199
+ if (!value) return 'Input path is required'
200
+ },
201
+ })
202
+
203
+ if (clack.isCancel(inputPathResult)) {
204
+ cancelAndExit()
205
+ }
206
+ inputPath = inputPathResult as string
207
+ }
208
+
209
+ // Prompt for output directory
210
+ let outputPath: string
211
+ if (yes) {
212
+ outputPath = initDefaults.outputPath
213
+ clack.log.info(`Using output path: ${styleText('cyan', outputPath)}`)
214
+ } else {
215
+ const outputPathResult = await clack.text({
216
+ message: 'Where should the generated files be output?',
217
+ placeholder: initDefaults.outputPath,
218
+ defaultValue: initDefaults.outputPath,
219
+ validate: (value) => {
220
+ if (!value) return 'Output path is required'
221
+ },
222
+ })
223
+
224
+ if (clack.isCancel(outputPathResult)) {
225
+ cancelAndExit()
226
+ }
227
+ outputPath = outputPathResult as string
228
+ }
229
+
230
+ // Plugin selection
231
+ let selectedPlugins: PluginOption[]
232
+ if (yes) {
233
+ selectedPlugins = availablePlugins.filter((plugin) => (initDefaults.plugins as readonly string[]).includes(plugin.value))
234
+ clack.log.info(`Using plugins: ${styleText('cyan', selectedPlugins.map((p) => p.label).join(', '))}`)
235
+ } else {
236
+ const selectedPluginValues = await clack.multiselect({
237
+ message: 'Select plugins to use:',
238
+ options: availablePlugins.map((plugin) => ({
239
+ value: plugin.value,
240
+ label: plugin.label,
241
+ hint: plugin.hint,
242
+ })),
243
+ initialValues: [...initDefaults.plugins],
244
+ required: true,
245
+ })
246
+
247
+ if (clack.isCancel(selectedPluginValues)) {
248
+ cancelAndExit()
249
+ }
250
+
251
+ selectedPlugins = availablePlugins.filter((plugin) => (selectedPluginValues as string[]).includes(plugin.value))
252
+ }
253
+
254
+ // Ensure plugin-oas is always included
255
+ if (!selectedPlugins.find((p) => p.value === 'plugin-oas')) {
256
+ selectedPlugins.unshift(availablePlugins.find((p) => p.value === 'plugin-oas')!)
257
+ }
258
+
259
+ // Install packages
260
+ const packagesToInstall = ['@kubb/core', '@kubb/cli', '@kubb/agent', ...selectedPlugins.map((p) => p.packageName)]
261
+
262
+ const spinner = clack.spinner()
263
+ spinner.start(`Installing ${packagesToInstall.length} packages with ${packageManager.name}`)
264
+
265
+ try {
266
+ await installPackages(packagesToInstall, packageManager, cwd)
267
+ spinner.stop(`Installed ${packagesToInstall.length} packages`)
268
+ } catch (error) {
269
+ spinner.stop('Installation failed')
270
+ throw error
271
+ }
272
+
273
+ // Generate config file
274
+ const configSpinner = clack.spinner()
275
+ configSpinner.start('Creating kubb.config.ts')
276
+
277
+ const configContent = generateConfigFile(selectedPlugins, inputPath, outputPath)
278
+ const configPath = path.join(cwd, 'kubb.config.ts')
279
+
280
+ if (fs.existsSync(configPath)) {
281
+ configSpinner.stop('kubb.config.ts already exists')
282
+
283
+ if (!yes) {
284
+ const shouldOverwrite = await clack.confirm({
285
+ message: 'kubb.config.ts already exists. Overwrite?',
286
+ initialValue: false,
287
+ })
288
+
289
+ if (clack.isCancel(shouldOverwrite) || !shouldOverwrite) {
290
+ cancelAndExit('Keeping existing configuration. Packages have been installed.')
291
+ }
292
+ }
293
+
294
+ configSpinner.start('Overwriting kubb.config.ts')
295
+ }
296
+
297
+ fs.writeFileSync(configPath, configContent, 'utf-8')
298
+
299
+ configSpinner.stop('Created kubb.config.ts')
300
+
301
+ clack.outro(
302
+ styleText('green', '✓ All set!') +
303
+ '\n\n' +
304
+ styleText('dim', 'Next steps:') +
305
+ '\n' +
306
+ styleText('cyan', ` 1. Make sure your OpenAPI spec is at: ${inputPath}`) +
307
+ '\n' +
308
+ styleText('cyan', ' 2. Generate code with: npx kubb generate') +
309
+ '\n' +
310
+ styleText('cyan', ' Or start a stream server with: npx kubb start') +
311
+ '\n' +
312
+ styleText('cyan', ` 3. Find generated files in: ${outputPath}`) +
313
+ '\n\n' +
314
+ styleText('dim', `Using ${packageManager.name} • Kubb v${version}`),
315
+ )
316
+ } catch (error) {
317
+ clack.log.error(styleText('red', 'An error occurred during initialization'))
318
+ if (error instanceof Error) {
319
+ clack.log.error(error.message)
320
+ }
321
+ process.exit(1)
322
+ }
323
+ }
@@ -0,0 +1,32 @@
1
+ import process from 'node:process'
2
+ import { styleText } from 'node:util'
3
+ import { getErrorMessage } from '@internals/utils'
4
+ import type * as McpModule from '@kubb/mcp'
5
+ import { jiti } from '../utils/jiti.ts'
6
+ import { buildTelemetryEvent, sendTelemetry } from '../utils/telemetry.ts'
7
+
8
+ type McpOptions = {
9
+ version: string
10
+ }
11
+
12
+ export async function runMcp({ version }: McpOptions): Promise<void> {
13
+ let mod: typeof McpModule
14
+ try {
15
+ mod = (await jiti.import('@kubb/mcp', { default: true })) as typeof McpModule
16
+ } catch (_e) {
17
+ console.error(`Import of '@kubb/mcp' is required to start the MCP server`)
18
+ process.exit(1)
19
+ }
20
+
21
+ const { run } = mod
22
+ const hrStart = process.hrtime()
23
+ try {
24
+ console.log('⏳ Starting MCP server...')
25
+ console.warn(styleText('yellow', 'This feature is still under development — use with caution'))
26
+ run()
27
+ await sendTelemetry(buildTelemetryEvent({ command: 'mcp', kubbVersion: version, hrStart, status: 'success' }))
28
+ } catch (error) {
29
+ await sendTelemetry(buildTelemetryEvent({ command: 'mcp', kubbVersion: version, hrStart, status: 'failed' }))
30
+ console.error(getErrorMessage(error))
31
+ }
32
+ }
@@ -0,0 +1,35 @@
1
+ import process from 'node:process'
2
+ import { getErrorMessage } from '@internals/utils'
3
+ import type * as OasModule from '@kubb/oas'
4
+ import { jiti } from '../utils/jiti.ts'
5
+ import { buildTelemetryEvent, sendTelemetry } from '../utils/telemetry.ts'
6
+
7
+ type ValidateOptions = {
8
+ input: string
9
+ version: string
10
+ }
11
+
12
+ export async function runValidate({ input, version }: ValidateOptions): Promise<void> {
13
+ let mod: typeof OasModule
14
+ try {
15
+ mod = (await jiti.import('@kubb/oas', { default: true })) as typeof OasModule
16
+ } catch (_e) {
17
+ console.error(`Import of '@kubb/oas' is required to do validation`)
18
+ process.exit(1)
19
+ }
20
+
21
+ const { parse } = mod
22
+ const hrStart = process.hrtime()
23
+ try {
24
+ const oas = await parse(input)
25
+ await oas.validate()
26
+
27
+ await sendTelemetry(buildTelemetryEvent({ command: 'validate', kubbVersion: version, hrStart, status: 'success' }))
28
+ console.log('✅ Validation success')
29
+ } catch (error) {
30
+ await sendTelemetry(buildTelemetryEvent({ command: 'validate', kubbVersion: version, hrStart, status: 'failed' }))
31
+ console.error('❌ Validation failed')
32
+ console.error(getErrorMessage(error))
33
+ process.exit(1)
34
+ }
35
+ }
@@ -10,8 +10,8 @@ export class ClackWritable extends Writable {
10
10
 
11
11
  this.taskLog = taskLog
12
12
  }
13
- _write(chunk: any, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
14
- this.taskLog.message(`${styleText('dim', chunk?.toString())}`)
13
+ _write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
14
+ this.taskLog.message(`${styleText('dim', chunk.toString())}`)
15
15
  callback()
16
16
  }
17
17
  }
@@ -1,8 +1,8 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { styleText } from 'node:util'
3
+ import type { AsyncEventEmitter } from '@internals/utils'
4
+ import { tokenize } from '@internals/utils'
3
5
  import type { Config, KubbEvents } from '@kubb/core'
4
- import type { AsyncEventEmitter } from '@kubb/core/utils'
5
- import { tokenize } from '@kubb/core/utils'
6
6
 
7
7
  type ExecutingHooksProps = {
8
8
  hooks: NonNullable<Config['hooks']>
@@ -20,14 +20,26 @@ export async function executeHooks({ hooks, events }: ExecutingHooksProps): Prom
20
20
  }
21
21
 
22
22
  const hookId = createHash('sha256').update(command).digest('hex')
23
- await events.emit('hook:start', { id: hookId, command: cmd, args })
24
23
 
25
- await events.onOnce('hook:end', async ({ success, error }) => {
26
- if (!success) {
27
- throw error
24
+ // Wire up the hook:end listener BEFORE emitting hook:start to avoid the race condition
25
+ // where hook:end fires synchronously inside emit('hook:start') before the listener is registered.
26
+ const hookEndPromise = new Promise<void>((resolve, reject) => {
27
+ const handler = ({ id, success, error }: { id?: string; command: string; args?: readonly string[]; success: boolean; error: Error | null }) => {
28
+ if (id !== hookId) return
29
+ events.off('hook:end', handler)
30
+ if (!success) {
31
+ reject(error ?? new Error(`Hook failed: ${command}`))
32
+ return
33
+ }
34
+ events
35
+ .emit('success', `${styleText('dim', command)} successfully executed`)
36
+ .then(resolve)
37
+ .catch(reject)
28
38
  }
29
-
30
- await events.emit('success', `${styleText('dim', command)} successfully executed`)
39
+ events.on('hook:end', handler)
31
40
  })
41
+
42
+ await events.emit('hook:start', { id: hookId, command: cmd, args })
43
+ await hookEndPromise
32
44
  }
33
45
  }
@@ -2,24 +2,23 @@ import type { defineConfig, UserConfig } from '@kubb/core'
2
2
  import { cosmiconfig } from 'cosmiconfig'
3
3
  import { createJiti } from 'jiti'
4
4
 
5
- export type CosmiconfigResult = {
5
+ type CosmiconfigResult = {
6
6
  filepath: string
7
7
  isEmpty?: boolean
8
8
  config: ReturnType<typeof defineConfig> | UserConfig
9
9
  }
10
10
 
11
- const tsLoader = async (configFile: string) => {
12
- const jiti = createJiti(import.meta.url, {
13
- jsx: {
14
- runtime: 'automatic',
15
- importSource: '@kubb/react-fabric',
16
- },
17
- sourceMaps: true,
18
- interopDefault: true,
19
- })
11
+ const jiti = createJiti(import.meta.url, {
12
+ jsx: {
13
+ runtime: 'automatic',
14
+ importSource: '@kubb/react-fabric',
15
+ },
16
+ sourceMaps: true,
17
+ interopDefault: true,
18
+ })
20
19
 
20
+ const tsLoader = async (configFile: string) => {
21
21
  const mod = await jiti.import(configFile, { default: true })
22
-
23
22
  return mod
24
23
  }
25
24
 
@@ -1,81 +1 @@
1
- import { styleText } from 'node:util'
2
-
3
- /**
4
- * ANSI True Color (24-bit) utilities for terminal output
5
- * Supports hex color codes without external dependencies like chalk
6
- */
7
-
8
- /**
9
- * Convert hex color to ANSI 24-bit true color escape sequence
10
- * @param color - Hex color code (with or without #), e.g., '#FF5500' or 'FF5500'
11
- * @returns Function that wraps text with the color
12
- */
13
- function hex(color: string): (text: string) => string {
14
- const cleanHex = color.replace('#', '')
15
- const r = Number.parseInt(cleanHex.slice(0, 2), 16)
16
- const g = Number.parseInt(cleanHex.slice(2, 4), 16)
17
- const b = Number.parseInt(cleanHex.slice(4, 6), 16)
18
-
19
- // Default to white (255) if parsing fails (NaN)
20
- const safeR = Number.isNaN(r) ? 255 : r
21
- const safeG = Number.isNaN(g) ? 255 : g
22
- const safeB = Number.isNaN(b) ? 255 : b
23
-
24
- return (text: string) => `\x1b[38;2;${safeR};${safeG};${safeB}m${text}\x1b[0m`
25
- }
26
-
27
- function hexToRgb(color: string) {
28
- const c = color.replace('#', '')
29
- return { r: Number.parseInt(c.slice(0, 2), 16), g: Number.parseInt(c.slice(2, 4), 16), b: Number.parseInt(c.slice(4, 6), 16) }
30
- }
31
-
32
- function gradient(colors: string[]) {
33
- return (text: string) => {
34
- const chars = [...text]
35
- return chars
36
- .map((char, i) => {
37
- const t = chars.length <= 1 ? 0 : i / (chars.length - 1)
38
- const seg = Math.min(Math.floor(t * (colors.length - 1)), colors.length - 2)
39
- const lt = t * (colors.length - 1) - seg
40
- const from = hexToRgb(colors[seg]!)
41
- const to = hexToRgb(colors[seg + 1]!)
42
- const r = Math.round(from.r + (to.r - from.r) * lt)
43
- const g = Math.round(from.g + (to.g - from.g) * lt)
44
- const b = Math.round(from.b + (to.b - from.b) * lt)
45
- return `\x1b[38;2;${r};${g};${b}m${char}\x1b[0m`
46
- })
47
- .join('')
48
- }
49
- }
50
-
51
- // Custom Color Palette for "Wooden" Depth
52
- const colors = {
53
- lid: hex('#F55A17'), // Dark Wood
54
- woodTop: hex('#F5A217'), // Bright Orange (Light source)
55
- woodMid: hex('#F58517'), // Main Orange
56
- woodBase: hex('#B45309'), // Shadow Orange
57
- eye: hex('#FFFFFF'), // Deep Slate
58
- highlight: hex('#adadc6'), // Eye shine
59
- blush: hex('#FDA4AF'), // Soft Rose
60
- }
61
-
62
- /**
63
- * Generates the Kubb mascot face welcome message
64
- * @param version - The version string to display
65
- * @returns Formatted mascot face string
66
- */
67
- export function getIntro({ title, description, version, areEyesOpen }: { title: string; description: string; version: string; areEyesOpen: boolean }): string {
68
- // Use gradient-string for the KUBB version text
69
- const kubbVersion = gradient(['#F58517', '#F5A217', '#F55A17'])(`KUBB v${version}`)
70
-
71
- const eyeTop = areEyesOpen ? colors.eye('█▀█') : colors.eye('───')
72
- const eyeBottom = areEyesOpen ? colors.eye('▀▀▀') : colors.eye('───')
73
-
74
- return `
75
- ${colors.lid('▄▄▄▄▄▄▄▄▄▄▄▄▄')}
76
- ${colors.woodTop('█ ')}${colors.highlight('▄▄')}${colors.woodTop(' ')}${colors.highlight('▄▄')}${colors.woodTop(' █')} ${kubbVersion}
77
- ${colors.woodMid('█ ')}${eyeTop}${colors.woodMid(' ')}${eyeTop}${colors.woodMid(' █')} ${styleText('gray', title)}
78
- ${colors.woodMid('█ ')}${eyeBottom}${colors.woodMid(' ')}${colors.blush('◡')}${colors.woodMid(' ')}${eyeBottom}${colors.woodMid(' █')} ${styleText('yellow', '➜')} ${styleText('white', description)}
79
- ${colors.woodBase('▀▀▀▀▀▀▀▀▀▀▀▀▀')}
80
- `
81
- }
1
+ export { getIntro } from '@internals/utils'
@@ -1,8 +1,8 @@
1
1
  import path from 'node:path'
2
2
  import { styleText } from 'node:util'
3
+ import { formatHrtime, randomCliColor } from '@internals/utils'
3
4
  import type { Config, Plugin } from '@kubb/core'
4
- import { formatHrtime } from '@kubb/core/utils'
5
- import { randomCliColor } from './randomColor.ts'
5
+ import { SUMMARY_MAX_BAR_LENGTH, SUMMARY_TIME_SCALE_DIVISOR } from '../constants.ts'
6
6
 
7
7
  type SummaryProps = {
8
8
  failedPlugins: Set<{ plugin: Plugin; error: Error }>
@@ -16,7 +16,7 @@ type SummaryProps = {
16
16
  export function getSummary({ failedPlugins, filesCreated, status, hrStart, config, pluginTimings }: SummaryProps): string[] {
17
17
  const duration = formatHrtime(hrStart)
18
18
 
19
- const pluginsCount = config.plugins?.length || 0
19
+ const pluginsCount = config.plugins?.length ?? 0
20
20
  const successCount = pluginsCount - failedPlugins.size
21
21
 
22
22
  const meta = {
@@ -24,8 +24,8 @@ export function getSummary({ failedPlugins, filesCreated, status, hrStart, confi
24
24
  status === 'success'
25
25
  ? `${styleText('green', `${successCount} successful`)}, ${pluginsCount} total`
26
26
  : `${styleText('green', `${successCount} successful`)}, ${styleText('red', `${failedPlugins.size} failed`)}, ${pluginsCount} total`,
27
- pluginsFailed: status === 'failed' ? [...failedPlugins]?.map(({ plugin }) => randomCliColor(plugin.name))?.join(', ') : undefined,
28
- filesCreated: filesCreated,
27
+ pluginsFailed: status === 'failed' ? [...failedPlugins].map(({ plugin }) => randomCliColor(plugin.name)).join(', ') : undefined,
28
+ filesCreated,
29
29
  time: styleText('green', duration),
30
30
  output: path.isAbsolute(config.root) ? path.resolve(config.root, config.output.path) : config.root,
31
31
  } as const
@@ -49,22 +49,17 @@ export function getSummary({ failedPlugins, filesCreated, status, hrStart, confi
49
49
  summaryLines.push(`${labels.generated.padEnd(maxLength + 2)} ${meta.filesCreated} files in ${meta.time}`)
50
50
 
51
51
  if (pluginTimings && pluginTimings.size > 0) {
52
- const TIME_SCALE_DIVISOR = 100
53
- const MAX_BAR_LENGTH = 10
54
-
55
52
  const sortedTimings = Array.from(pluginTimings.entries()).sort((a, b) => b[1] - a[1])
56
53
 
57
- if (sortedTimings.length > 0) {
58
- summaryLines.push(`${labels.pluginTimings}`)
54
+ summaryLines.push(`${labels.pluginTimings}`)
59
55
 
60
- sortedTimings.forEach(([name, time]) => {
61
- const timeStr = time >= 1000 ? `${(time / 1000).toFixed(2)}s` : `${Math.round(time)}ms`
62
- const barLength = Math.min(Math.ceil(time / TIME_SCALE_DIVISOR), MAX_BAR_LENGTH)
63
- const bar = styleText('dim', '█'.repeat(barLength))
56
+ sortedTimings.forEach(([name, time]) => {
57
+ const timeStr = time >= 1000 ? `${(time / 1000).toFixed(2)}s` : `${Math.round(time)}ms`
58
+ const barLength = Math.min(Math.ceil(time / SUMMARY_TIME_SCALE_DIVISOR), SUMMARY_MAX_BAR_LENGTH)
59
+ const bar = styleText('dim', '█'.repeat(barLength))
64
60
 
65
- summaryLines.push(`${styleText('dim', '•')} ${name.padEnd(maxLength + 1)}${bar} ${timeStr}`)
66
- })
67
- }
61
+ summaryLines.push(`${styleText('dim', '•')} ${name.padEnd(maxLength + 1)}${bar} ${timeStr}`)
62
+ })
68
63
  }
69
64
 
70
65
  summaryLines.push(`${labels.output.padEnd(maxLength + 2)} ${meta.output}`)
@@ -0,0 +1,9 @@
1
+ import { createJiti } from 'jiti'
2
+
3
+ /**
4
+ * Shared jiti instance for dynamic ESM/TS imports across CLI commands.
5
+ * Created once at module scope to avoid the overhead of repeated instantiation.
6
+ */
7
+ export const jiti = createJiti(import.meta.url, {
8
+ sourceMaps: true,
9
+ })
@@ -1,7 +1,7 @@
1
- import { spawn } from 'node:child_process'
2
1
  import fs from 'node:fs'
3
2
  import path from 'node:path'
4
- import type { PackageManagerInfo, PackageManagerName } from '@kubb/core'
3
+ import type { PackageManagerInfo, PackageManagerName } from '@internals/utils'
4
+ import { spawnAsync } from '@internals/utils'
5
5
 
6
6
  export function hasPackageJson(cwd: string = process.cwd()): boolean {
7
7
  return fs.existsSync(path.join(cwd, 'package.json'))
@@ -15,9 +15,9 @@ export async function initPackageJson(cwd: string, packageManager: PackageManage
15
15
  bun: ['init', '-y'],
16
16
  }
17
17
 
18
- spawn(packageManager.name, commands[packageManager.name], { stdio: 'inherit', cwd })
18
+ await spawnAsync(packageManager.name, commands[packageManager.name], { cwd })
19
19
  }
20
20
 
21
21
  export async function installPackages(packages: string[], packageManager: PackageManagerInfo, cwd: string = process.cwd()): Promise<void> {
22
- spawn(packageManager.name, [...packageManager.installCommand, ...packages], { stdio: 'inherit', cwd })
22
+ await spawnAsync(packageManager.name, [...packageManager.installCommand, ...packages], { cwd })
23
23
  }