@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
@@ -1,13 +1,76 @@
1
+ import { styleText } from 'node:util'
2
+ import { canUseTTY, formatHrtime, isGitHubActions } from '@internals/utils'
1
3
  import type { Logger, LoggerContext, LoggerOptions } from '@kubb/core'
2
- import { LogLevel } from '@kubb/core'
4
+ import { logLevel as logLevelMap } from '@kubb/core'
3
5
  import { clackLogger } from './clackLogger.ts'
4
- import { canUseTTY, isGitHubActions } from './envDetection.ts'
5
6
  import { fileSystemLogger } from './fileSystemLogger.ts'
6
7
  import { githubActionsLogger } from './githubActionsLogger.ts'
7
8
  import { plainLogger } from './plainLogger.ts'
8
9
  import type { LoggerType } from './types.ts'
9
10
 
10
- export function detectLogger(): LoggerType {
11
+ /**
12
+ * Optionally prefix a message with a [HH:MM:SS] timestamp when logLevel >= verbose.
13
+ * Shared across all logger adapters to avoid duplication.
14
+ */
15
+ export function formatMessage(message: string, logLevel: number): string {
16
+ if (logLevel >= logLevelMap.verbose) {
17
+ const timestamp = new Date().toLocaleTimeString('en-US', {
18
+ hour12: false,
19
+ hour: '2-digit',
20
+ minute: '2-digit',
21
+ second: '2-digit',
22
+ })
23
+ return `${styleText('dim', `[${timestamp}]`)} ${message}`
24
+ }
25
+ return message
26
+ }
27
+
28
+ type ProgressState = {
29
+ totalPlugins: number
30
+ completedPlugins: number
31
+ failedPlugins: number
32
+ totalFiles: number
33
+ processedFiles: number
34
+ hrStart: [number, number]
35
+ }
36
+
37
+ /**
38
+ * Build the progress summary line shared by clack and GitHub Actions loggers.
39
+ * Returns null when there is nothing to display.
40
+ */
41
+ export function buildProgressLine(state: ProgressState): string | null {
42
+ const parts: string[] = []
43
+ const duration = formatHrtime(state.hrStart)
44
+
45
+ if (state.totalPlugins > 0) {
46
+ const pluginStr =
47
+ state.failedPlugins > 0
48
+ ? `Plugins ${styleText('green', state.completedPlugins.toString())}/${state.totalPlugins} ${styleText('red', `(${state.failedPlugins} failed)`)}`
49
+ : `Plugins ${styleText('green', state.completedPlugins.toString())}/${state.totalPlugins}`
50
+ parts.push(pluginStr)
51
+ }
52
+
53
+ if (state.totalFiles > 0) {
54
+ parts.push(`Files ${styleText('green', state.processedFiles.toString())}/${state.totalFiles}`)
55
+ }
56
+
57
+ if (parts.length === 0) {
58
+ return null
59
+ }
60
+
61
+ parts.push(`${styleText('green', duration)} elapsed`)
62
+ return parts.join(styleText('dim', ' | '))
63
+ }
64
+
65
+ /**
66
+ * Join a command and its optional args into a single display string.
67
+ * e.g. ("prettier", ["--write", "."]) → "prettier --write ."
68
+ */
69
+ export function formatCommandWithArgs(command: string, args?: readonly string[]): string {
70
+ return args?.length ? `${command} ${args.join(' ')}` : command
71
+ }
72
+
73
+ function detectLogger(): LoggerType {
11
74
  if (isGitHubActions()) {
12
75
  return 'github-actions'
13
76
  }
@@ -35,7 +98,7 @@ export async function setupLogger(context: LoggerContext, { logLevel }: LoggerOp
35
98
  // Install primary logger
36
99
  const cleanup = await logger.install(context, { logLevel })
37
100
 
38
- if (logLevel >= LogLevel.debug) {
101
+ if (logLevel >= logLevelMap.debug) {
39
102
  await fileSystemLogger.install(context, { logLevel })
40
103
  }
41
104
 
@@ -0,0 +1,100 @@
1
+ import net from 'node:net'
2
+ import path from 'node:path'
3
+ import * as process from 'node:process'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { styleText } from 'node:util'
6
+ import * as clack from '@clack/prompts'
7
+ import { spawnAsync } from '@internals/utils'
8
+ import { agentDefaults } from '../constants.ts'
9
+ import { buildTelemetryEvent, sendTelemetry } from '../utils/telemetry.ts'
10
+
11
+ type AgentStartOptions = {
12
+ port: string | undefined
13
+ host: string
14
+ configPath: string
15
+ allowWrite: boolean
16
+ allowAll: boolean
17
+ version: string
18
+ }
19
+
20
+ function isPortAvailable(port: number, host: string): Promise<boolean> {
21
+ return new Promise((resolve) => {
22
+ const server = net.createServer()
23
+ server.once('error', () => resolve(false))
24
+ server.once('listening', () => {
25
+ server.close()
26
+ resolve(true)
27
+ })
28
+ server.listen(port, host)
29
+ })
30
+ }
31
+
32
+ export async function runAgentStart({ port, host, configPath, allowWrite, allowAll, version }: AgentStartOptions): Promise<void> {
33
+ const hrStart = process.hrtime()
34
+
35
+ try {
36
+ // Load .env file into process.env using Node.js built-in (v20.12.0+)
37
+ try {
38
+ process.loadEnvFile()
39
+ } catch {
40
+ // .env file may not exist; ignore
41
+ }
42
+
43
+ // Resolve the @kubb/agent package path
44
+ const agentPkgUrl = import.meta.resolve('@kubb/agent/package.json')
45
+ const agentPkgPath = fileURLToPath(agentPkgUrl)
46
+ const agentDir = path.dirname(agentPkgPath)
47
+ const serverPath = path.join(agentDir, agentDefaults.serverEntryPath)
48
+
49
+ // CLI params take priority over process.env; process.env fills in what the CLI didn't specify;
50
+ // agentDefaults are the last resort. Build env as: defaults ← process.env ← CLI.
51
+ const PORT = port !== undefined ? port : (process.env.PORT ?? agentDefaults.port)
52
+ const HOST = host !== agentDefaults.host ? host : (process.env.HOST ?? agentDefaults.host)
53
+ const KUBB_AGENT_ROOT = process.env.KUBB_AGENT_ROOT ?? process.cwd()
54
+ const KUBB_AGENT_CONFIG = configPath !== agentDefaults.configFile ? configPath : (process.env.KUBB_AGENT_CONFIG ?? agentDefaults.configFile)
55
+ const KUBB_AGENT_ALLOW_WRITE = allowAll || allowWrite ? 'true' : (process.env.KUBB_AGENT_ALLOW_WRITE ?? 'false')
56
+ const KUBB_AGENT_ALLOW_ALL = allowAll ? 'true' : (process.env.KUBB_AGENT_ALLOW_ALL ?? 'false')
57
+ const KUBB_AGENT_TOKEN = process.env.KUBB_AGENT_TOKEN
58
+ const KUBB_AGENT_RETRY_TIMEOUT = process.env.KUBB_AGENT_RETRY_TIMEOUT ?? agentDefaults.retryTimeout
59
+ const KUBB_STUDIO_URL = process.env.KUBB_STUDIO_URL ?? agentDefaults.studioUrl
60
+
61
+ const env = {
62
+ ...process.env,
63
+ PORT,
64
+ HOST,
65
+ KUBB_AGENT_ROOT,
66
+ KUBB_AGENT_CONFIG,
67
+ KUBB_AGENT_ALLOW_WRITE,
68
+ KUBB_AGENT_ALLOW_ALL,
69
+ KUBB_AGENT_TOKEN,
70
+ KUBB_AGENT_RETRY_TIMEOUT,
71
+ KUBB_STUDIO_URL,
72
+ }
73
+
74
+ clack.log.step(styleText('cyan', 'Starting agent server...'))
75
+ clack.log.info(styleText('dim', `Config: ${KUBB_AGENT_CONFIG}`))
76
+ clack.log.info(styleText('dim', `Host: ${HOST}`))
77
+ clack.log.info(styleText('dim', `Port: ${PORT}`))
78
+ if (!KUBB_AGENT_ALLOW_WRITE && !KUBB_AGENT_ALLOW_ALL) {
79
+ clack.log.warn(styleText('yellow', 'Filesystem writes disabled. Use --allow-write or --allow-all to enable.'))
80
+ }
81
+
82
+ if (!(await isPortAvailable(Number(PORT), HOST))) {
83
+ clack.log.error(styleText('red', `Port ${PORT} is already in use. Stop the existing process or choose a different port with --port.`))
84
+ process.exit(1)
85
+ }
86
+
87
+ // Spawns the server as a detached background process so the CLI can exit independently.
88
+ await spawnAsync('node', [serverPath], {
89
+ env,
90
+ cwd: process.cwd(),
91
+ })
92
+
93
+ await sendTelemetry(buildTelemetryEvent({ command: 'agent', kubbVersion: version, hrStart, status: 'success' }))
94
+ } catch (error) {
95
+ await sendTelemetry(buildTelemetryEvent({ command: 'agent', kubbVersion: version, hrStart, status: 'failed' }))
96
+ clack.log.error(styleText('red', 'Failed to start agent server'))
97
+ console.error(error)
98
+ process.exit(1)
99
+ }
100
+ }
@@ -2,12 +2,31 @@ import { createHash } from 'node:crypto'
2
2
  import path from 'node:path'
3
3
  import process from 'node:process'
4
4
  import { styleText } from 'node:util'
5
- import { type Config, type KubbEvents, LogLevel, safeBuild, setup } from '@kubb/core'
6
- import type { AsyncEventEmitter } from '@kubb/core/utils'
7
- import { detectFormatter, detectLinter, formatters, linters } from '@kubb/core/utils'
5
+ import * as clack from '@clack/prompts'
6
+ import type { AsyncEventEmitter } from '@internals/utils'
7
+ import { AsyncEventEmitter as AsyncEventEmitterClass, executeIfOnline, toError } from '@internals/utils'
8
+ import {
9
+ type CLIOptions,
10
+ type Config,
11
+ detectFormatter,
12
+ detectLinter,
13
+ formatters,
14
+ getConfigs,
15
+ isInputPath,
16
+ type KubbEvents,
17
+ linters,
18
+ logLevel as logLevelMap,
19
+ PromiseManager,
20
+ safeBuild,
21
+ setup,
22
+ } from '@kubb/core'
8
23
  import { version } from '../../package.json'
24
+ import { KUBB_NPM_PACKAGE_URL } from '../constants.ts'
25
+ import { setupLogger } from '../loggers/utils.ts'
9
26
  import { executeHooks } from '../utils/executeHooks.ts'
27
+ import { getCosmiConfig } from '../utils/getCosmiConfig.ts'
10
28
  import { buildTelemetryEvent, sendTelemetry } from '../utils/telemetry.ts'
29
+ import { startWatcher } from '../utils/watcher.ts'
11
30
 
12
31
  type GenerateProps = {
13
32
  input?: string
@@ -16,7 +35,103 @@ type GenerateProps = {
16
35
  logLevel: number
17
36
  }
18
37
 
19
- export async function generate({ input, config: userConfig, events, logLevel }: GenerateProps): Promise<void> {
38
+ type ToolMap = typeof formatters | typeof linters
39
+
40
+ type RunToolPassOptions = {
41
+ toolValue: string
42
+ detect: () => Promise<string | undefined>
43
+ toolMap: ToolMap
44
+ /** Short noun used in "Auto-detected <toolLabel>:" message, e.g. "formatter" or "linter". */
45
+ toolLabel: string
46
+ /** Verb prefix for the success message, e.g. "Formatting" or "Linting". */
47
+ successPrefix: string
48
+ noToolMessage: string
49
+ configName: string | undefined
50
+ outputPath: string
51
+ logLevel: number
52
+ events: AsyncEventEmitter<KubbEvents>
53
+ onStart: () => Promise<void>
54
+ onEnd: () => Promise<void>
55
+ }
56
+
57
+ async function runToolPass({
58
+ toolValue,
59
+ detect,
60
+ toolMap,
61
+ toolLabel,
62
+ successPrefix,
63
+ noToolMessage,
64
+ configName,
65
+ outputPath,
66
+ logLevel,
67
+ events,
68
+ onStart,
69
+ onEnd,
70
+ }: RunToolPassOptions) {
71
+ await onStart()
72
+
73
+ let resolvedTool = toolValue
74
+ if (resolvedTool === 'auto') {
75
+ const detected = await detect()
76
+ if (!detected) {
77
+ await events.emit('warn', noToolMessage)
78
+ } else {
79
+ resolvedTool = detected
80
+ await events.emit('info', `Auto-detected ${toolLabel}: ${styleText('dim', resolvedTool)}`)
81
+ }
82
+ }
83
+
84
+ if (resolvedTool && resolvedTool !== 'auto' && resolvedTool in toolMap) {
85
+ const toolConfig = toolMap[resolvedTool as keyof ToolMap]
86
+
87
+ try {
88
+ const hookId = createHash('sha256').update([configName, resolvedTool].filter(Boolean).join('-')).digest('hex')
89
+
90
+ // Wire up the hook:end listener BEFORE emitting hook:start to avoid the race condition
91
+ // where hook:end fires synchronously inside emit('hook:start') before the listener is registered.
92
+ const hookEndPromise = new Promise<void>((resolve, reject) => {
93
+ const handler = ({ id, success, error }: { id?: string; command: string; args?: readonly string[]; success: boolean; error: Error | null }) => {
94
+ if (id !== hookId) return
95
+ events.off('hook:end', handler)
96
+ if (!success) {
97
+ reject(error ?? new Error(`${toolConfig.errorMessage}`))
98
+ return
99
+ }
100
+ events
101
+ .emit(
102
+ 'success',
103
+ [
104
+ `${successPrefix} with ${styleText('dim', resolvedTool)}`,
105
+ logLevel >= logLevelMap.info ? `on ${styleText('dim', outputPath)}` : undefined,
106
+ 'successfully',
107
+ ]
108
+ .filter(Boolean)
109
+ .join(' '),
110
+ )
111
+ .then(resolve)
112
+ .catch(reject)
113
+ }
114
+ events.on('hook:end', handler)
115
+ })
116
+
117
+ await events.emit('hook:start', {
118
+ id: hookId,
119
+ command: toolConfig.command,
120
+ args: toolConfig.args(outputPath),
121
+ })
122
+
123
+ await hookEndPromise
124
+ } catch (caughtError) {
125
+ const err = new Error(toolConfig.errorMessage)
126
+ err.cause = caughtError
127
+ await events.emit('error', err)
128
+ }
129
+ }
130
+
131
+ await onEnd()
132
+ }
133
+
134
+ async function generate({ input, config: userConfig, events, logLevel }: GenerateProps): Promise<void> {
20
135
  const inputPath = input ?? ('path' in userConfig.input ? userConfig.input.path : undefined)
21
136
  const hrStart = process.hrtime()
22
137
 
@@ -73,9 +188,9 @@ export async function generate({ input, config: userConfig, events, logLevel }:
73
188
  .map((it) => it.error),
74
189
  ].filter(Boolean)
75
190
 
76
- allErrors.forEach((err) => {
77
- events.emit('error', err)
78
- })
191
+ for (const err of allErrors) {
192
+ await events.emit('error', err)
193
+ }
79
194
 
80
195
  await events.emit('generation:end', config, files, sources)
81
196
 
@@ -84,7 +199,7 @@ export async function generate({ input, config: userConfig, events, logLevel }:
84
199
  filesCreated: files.length,
85
200
  status: 'failed',
86
201
  hrStart,
87
- pluginTimings: logLevel >= LogLevel.verbose ? pluginTimings : undefined,
202
+ pluginTimings: logLevel >= logLevelMap.verbose ? pluginTimings : undefined,
88
203
  })
89
204
 
90
205
  await sendTelemetry(
@@ -104,100 +219,40 @@ export async function generate({ input, config: userConfig, events, logLevel }:
104
219
  await events.emit('success', 'Generation successfully', inputPath)
105
220
  await events.emit('generation:end', config, files, sources)
106
221
 
107
- // formatting
108
- if (config.output.format) {
109
- await events.emit('format:start')
110
-
111
- let formatter = config.output.format
112
- if (formatter === 'auto') {
113
- const detectedFormatter = await detectFormatter()
114
- if (!detectedFormatter) {
115
- await events.emit('warn', 'No formatter found (biome, prettier, or oxfmt). Skipping formatting.')
116
- } else {
117
- formatter = detectedFormatter
118
- await events.emit('info', `Auto-detected formatter: ${styleText('dim', formatter)}`)
119
- }
120
- }
121
-
122
- if (formatter && formatter !== 'auto' && formatter in formatters) {
123
- const formatterConfig = formatters[formatter as keyof typeof formatters]
124
- const outputPath = path.resolve(config.root, config.output.path)
125
-
126
- try {
127
- const hookId = createHash('sha256').update([config.name, formatter].filter(Boolean).join('-')).digest('hex')
128
- await events.emit('hook:start', {
129
- id: hookId,
130
- command: formatterConfig.command,
131
- args: formatterConfig.args(outputPath),
132
- })
133
-
134
- await events.onOnce('hook:end', async ({ success, error }) => {
135
- if (!success) throw error
136
-
137
- await events.emit(
138
- 'success',
139
- [`Formatting with ${styleText('dim', formatter)}`, logLevel >= LogLevel.info ? `on ${styleText('dim', outputPath)}` : undefined, 'successfully']
140
- .filter(Boolean)
141
- .join(' '),
142
- )
143
- })
144
- } catch (caughtError) {
145
- const error = new Error(formatterConfig.errorMessage)
146
- error.cause = caughtError
147
- await events.emit('error', error)
148
- }
149
- }
222
+ const outputPath = path.resolve(config.root, config.output.path)
150
223
 
151
- await events.emit('format:end')
224
+ if (config.output.format) {
225
+ await runToolPass({
226
+ toolValue: config.output.format,
227
+ detect: detectFormatter,
228
+ toolMap: formatters,
229
+ toolLabel: 'formatter',
230
+ successPrefix: 'Formatting',
231
+ noToolMessage: 'No formatter found (biome, prettier, or oxfmt). Skipping formatting.',
232
+ configName: config.name,
233
+ outputPath,
234
+ logLevel,
235
+ events,
236
+ onStart: () => events.emit('format:start'),
237
+ onEnd: () => events.emit('format:end'),
238
+ })
152
239
  }
153
240
 
154
- // linting
155
241
  if (config.output.lint) {
156
- await events.emit('lint:start')
157
-
158
- // Detect linter if set to 'auto'
159
- let linter = config.output.lint
160
- if (linter === 'auto') {
161
- const detectedLinter = await detectLinter()
162
- if (!detectedLinter) {
163
- await events.emit('warn', 'No linter found (biome, oxlint, or eslint). Skipping linting.')
164
- } else {
165
- linter = detectedLinter
166
- await events.emit('info', `Auto-detected linter: ${styleText('dim', linter)}`)
167
- }
168
- }
169
-
170
- // Only proceed with linting if we have a valid linter
171
- if (linter && linter !== 'auto' && linter in linters) {
172
- const linterConfig = linters[linter as keyof typeof linters]
173
- const outputPath = path.resolve(config.root, config.output.path)
174
-
175
- try {
176
- const hookId = createHash('sha256').update([config.name, linter].filter(Boolean).join('-')).digest('hex')
177
- await events.emit('hook:start', {
178
- id: hookId,
179
- command: linterConfig.command,
180
- args: linterConfig.args(outputPath),
181
- })
182
-
183
- await events.onOnce('hook:end', async ({ success, error }) => {
184
- if (!success) throw error
185
-
186
- await events.emit(
187
- 'success',
188
- [`Linting with ${styleText('dim', linter)}`, logLevel >= LogLevel.info ? `on ${styleText('dim', outputPath)}` : undefined, 'successfully']
189
- .filter(Boolean)
190
- .join(' '),
191
- )
192
- })
193
- } catch (caughtError) {
194
- const error = new Error(linterConfig.errorMessage)
195
- error.cause = caughtError
196
- await events.emit('error', error)
197
- }
198
- }
199
-
200
- await events.emit('lint:end')
242
+ await runToolPass({
243
+ toolValue: config.output.lint,
244
+ detect: detectLinter,
245
+ toolMap: linters,
246
+ toolLabel: 'linter',
247
+ successPrefix: 'Linting',
248
+ noToolMessage: 'No linter found (biome, oxlint, or eslint). Skipping linting.',
249
+ configName: config.name,
250
+ outputPath,
251
+ logLevel,
252
+ events,
253
+ onStart: () => events.emit('lint:start'),
254
+ onEnd: () => events.emit('lint:end'),
255
+ })
201
256
  }
202
257
 
203
258
  if (config.hooks) {
@@ -207,12 +262,11 @@ export async function generate({ input, config: userConfig, events, logLevel }:
207
262
  await events.emit('hooks:end')
208
263
  }
209
264
 
210
- const generationStatus = failedPlugins.size > 0 || error ? 'failed' : 'success'
211
-
265
+ // Only reached when there are no failures (process.exit(1) is called above otherwise)
212
266
  await events.emit('generation:summary', config, {
213
267
  failedPlugins,
214
268
  filesCreated: files.length,
215
- status: generationStatus,
269
+ status: 'success',
216
270
  hrStart,
217
271
  pluginTimings,
218
272
  })
@@ -223,8 +277,75 @@ export async function generate({ input, config: userConfig, events, logLevel }:
223
277
  plugins: pluginManager.plugins.map((p) => ({ name: p.name, options: p.options as Record<string, unknown> })),
224
278
  hrStart,
225
279
  filesCreated: files.length,
226
- status: generationStatus,
280
+ status: 'success',
227
281
  })
228
282
 
229
283
  await sendTelemetry(telemetryEvent)
230
284
  }
285
+
286
+ type GenerateCommandOptions = {
287
+ input?: string
288
+ configPath?: string
289
+ logLevel: string
290
+ watch: boolean
291
+ }
292
+
293
+ export async function runGenerateCommand({ input, configPath, logLevel: logLevelKey, watch }: GenerateCommandOptions): Promise<void> {
294
+ const logLevel = logLevelMap[logLevelKey as keyof typeof logLevelMap] ?? logLevelMap.info
295
+ const events = new AsyncEventEmitterClass<KubbEvents>()
296
+ const promiseManager = new PromiseManager()
297
+
298
+ await setupLogger(events, { logLevel })
299
+
300
+ await executeIfOnline(async () => {
301
+ try {
302
+ const res = await fetch(KUBB_NPM_PACKAGE_URL)
303
+ const data = (await res.json()) as { version: string }
304
+ const latestVersion = data.version
305
+
306
+ if (latestVersion && version < latestVersion) {
307
+ await events.emit('version:new', version, latestVersion)
308
+ }
309
+ } catch {
310
+ // Ignore network errors for version check
311
+ }
312
+ })
313
+
314
+ try {
315
+ const result = await getCosmiConfig('kubb', configPath)
316
+ const configs = await getConfigs(result.config, { input } as CLIOptions)
317
+
318
+ await events.emit('config:start')
319
+ await events.emit('info', 'Config loaded', path.relative(process.cwd(), result.filepath))
320
+ await events.emit('success', 'Config loaded successfully', path.relative(process.cwd(), result.filepath))
321
+ await events.emit('config:end', configs)
322
+
323
+ await events.emit('lifecycle:start', version)
324
+
325
+ const promises = configs.map((config) => {
326
+ return async () => {
327
+ if (isInputPath(config) && watch) {
328
+ await startWatcher([input || config.input.path], async (paths) => {
329
+ // remove to avoid duplicate listeners after each change
330
+ events.removeAll()
331
+
332
+ await generate({ input, config, logLevel, events })
333
+
334
+ clack.log.step(styleText('yellow', `Watching for changes in ${paths.join(' and ')}`))
335
+ })
336
+
337
+ return
338
+ }
339
+
340
+ await generate({ input, config, logLevel, events })
341
+ }
342
+ })
343
+
344
+ await promiseManager.run('seq', promises)
345
+
346
+ await events.emit('lifecycle:end')
347
+ } catch (error) {
348
+ await events.emit('error', toError(error))
349
+ process.exit(1)
350
+ }
351
+ }