@kubb/cli 4.11.3 → 4.12.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.
- package/dist/generate-BfIvo80v.js +1249 -0
- package/dist/generate-BfIvo80v.js.map +1 -0
- package/dist/generate-BoZaeXSl.cjs +1256 -0
- package/dist/generate-BoZaeXSl.cjs.map +1 -0
- package/dist/index.cjs +9 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -30
- package/dist/index.js.map +1 -1
- package/dist/{mcp-BIRDY8xn.js → mcp-LLlOFV3c.js} +5 -6
- package/dist/mcp-LLlOFV3c.js.map +1 -0
- package/dist/{mcp-BQjDRDXR.cjs → mcp-N1IVyiXf.cjs} +5 -7
- package/dist/mcp-N1IVyiXf.cjs.map +1 -0
- package/dist/package-B1rQiz1w.js +6 -0
- package/dist/package-B1rQiz1w.js.map +1 -0
- package/dist/package-CngLazMY.cjs +12 -0
- package/dist/package-CngLazMY.cjs.map +1 -0
- package/dist/{validate-0i6Q9eIy.js → validate-BptoQ-63.js} +5 -6
- package/dist/validate-BptoQ-63.js.map +1 -0
- package/dist/{validate-6F-VPZR7.cjs → validate-Dn6hZ7Z4.cjs} +5 -7
- package/dist/validate-Dn6hZ7Z4.cjs.map +1 -0
- package/package.json +7 -7
- package/src/commands/generate.ts +52 -65
- package/src/commands/mcp.ts +4 -5
- package/src/commands/validate.ts +4 -5
- package/src/index.ts +8 -23
- package/src/loggers/clackLogger.ts +433 -0
- package/src/loggers/envDetection.ts +28 -0
- package/src/loggers/fileSystemLogger.ts +79 -0
- package/src/loggers/githubActionsLogger.ts +310 -0
- package/src/loggers/index.ts +5 -0
- package/src/loggers/plainLogger.ts +274 -0
- package/src/loggers/types.ts +1 -0
- package/src/loggers/utils.ts +43 -0
- package/src/runners/generate.ts +196 -208
- package/src/utils/Writables.ts +12 -8
- package/src/utils/executeHooks.ts +11 -18
- package/src/utils/getCosmiConfig.ts +6 -1
- package/src/utils/getSummary.ts +20 -42
- package/src/utils/randomColour.ts +26 -0
- package/src/utils/watcher.ts +2 -4
- package/dist/generate-CYBFB3tU.js +0 -221
- package/dist/generate-CYBFB3tU.js.map +0 -1
- package/dist/generate-CpBJ2Y-n.js +0 -342
- package/dist/generate-CpBJ2Y-n.js.map +0 -1
- package/dist/generate-DpHvARzf.cjs +0 -345
- package/dist/generate-DpHvARzf.cjs.map +0 -1
- package/dist/generate-KUqCSnZp.cjs +0 -225
- package/dist/generate-KUqCSnZp.cjs.map +0 -1
- package/dist/mcp-BIRDY8xn.js.map +0 -1
- package/dist/mcp-BQjDRDXR.cjs.map +0 -1
- package/dist/validate-0i6Q9eIy.js.map +0 -1
- package/dist/validate-6F-VPZR7.cjs.map +0 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { relative } from 'node:path'
|
|
2
|
+
import * as clack from '@clack/prompts'
|
|
3
|
+
import { defineLogger, LogLevel } from '@kubb/core'
|
|
4
|
+
import { execa } from 'execa'
|
|
5
|
+
import { default as gradientString } from 'gradient-string'
|
|
6
|
+
import pc from 'picocolors'
|
|
7
|
+
|
|
8
|
+
import { getSummary } from '../utils/getSummary.ts'
|
|
9
|
+
import { ClackWritable } from '../utils/Writables.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Clack adapter for local TTY environments
|
|
13
|
+
* Provides a beautiful CLI UI with flat structure inspired by Claude's CLI patterns
|
|
14
|
+
*/
|
|
15
|
+
export const clackLogger = defineLogger({
|
|
16
|
+
name: 'clack',
|
|
17
|
+
install(context, options) {
|
|
18
|
+
const logLevel = options?.logLevel || LogLevel.info
|
|
19
|
+
const activeProgress = new Map<string, { interval?: NodeJS.Timeout; progressBar: clack.ProgressResult }>()
|
|
20
|
+
const spinner = clack.spinner()
|
|
21
|
+
let isSpinning = false
|
|
22
|
+
|
|
23
|
+
function getMessage(message: string): string {
|
|
24
|
+
if (logLevel >= LogLevel.verbose) {
|
|
25
|
+
const timestamp = new Date().toLocaleTimeString('en-US', {
|
|
26
|
+
hour12: false,
|
|
27
|
+
hour: '2-digit',
|
|
28
|
+
minute: '2-digit',
|
|
29
|
+
second: '2-digit',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return [pc.dim(`[${timestamp}]`), message].join(' ')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return message
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function startSpinner(text?: string) {
|
|
39
|
+
spinner.start(text)
|
|
40
|
+
isSpinning = true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stopSpinner(text?: string) {
|
|
44
|
+
spinner.stop(text)
|
|
45
|
+
isSpinning = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
context.on('info', (message, info = '') => {
|
|
49
|
+
if (logLevel <= LogLevel.silent) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const text = getMessage([pc.blue('ℹ'), message, pc.dim(info)].join(' '))
|
|
54
|
+
|
|
55
|
+
if (isSpinning) {
|
|
56
|
+
spinner.message(text)
|
|
57
|
+
} else {
|
|
58
|
+
clack.log.info(text)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
context.on('success', (message, info = '') => {
|
|
63
|
+
if (logLevel <= LogLevel.silent) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const text = getMessage([pc.blue('✓'), message, logLevel >= LogLevel.info ? pc.dim(info) : undefined].filter(Boolean).join(' '))
|
|
68
|
+
|
|
69
|
+
if (isSpinning) {
|
|
70
|
+
stopSpinner(text)
|
|
71
|
+
} else {
|
|
72
|
+
clack.log.success(text)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
context.on('warn', (message, info) => {
|
|
77
|
+
if (logLevel < LogLevel.warn) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const text = getMessage([pc.yellow('⚠'), message, logLevel >= LogLevel.info ? pc.dim(info) : undefined].filter(Boolean).join(' '))
|
|
82
|
+
|
|
83
|
+
clack.log.warn(text)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
context.on('error', (error) => {
|
|
87
|
+
const caused = error.cause as Error
|
|
88
|
+
|
|
89
|
+
const text = [pc.red('✗'), error.message].join(' ')
|
|
90
|
+
|
|
91
|
+
if (isSpinning) {
|
|
92
|
+
stopSpinner(getMessage(text))
|
|
93
|
+
} else {
|
|
94
|
+
clack.log.error(getMessage(text))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Show stack trace in debug mode (first 3 frames)
|
|
98
|
+
if (logLevel >= LogLevel.debug && error.stack) {
|
|
99
|
+
const frames = error.stack.split('\n').slice(1, 4)
|
|
100
|
+
for (const frame of frames) {
|
|
101
|
+
clack.log.message(getMessage(pc.dim(frame.trim())))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (caused?.stack) {
|
|
105
|
+
clack.log.message(pc.dim(`└─ caused by ${caused.message}`))
|
|
106
|
+
|
|
107
|
+
const frames = caused.stack.split('\n').slice(1, 4)
|
|
108
|
+
for (const frame of frames) {
|
|
109
|
+
clack.log.message(getMessage(` ${pc.dim(frame.trim())}`))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
context.on('version:new', (version, latestVersion) => {
|
|
116
|
+
if (logLevel <= LogLevel.silent) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
clack.box(
|
|
121
|
+
`\`v${version}\` → \`v${latestVersion}\`
|
|
122
|
+
Run \`npm install -g @kubb/cli\` to update`,
|
|
123
|
+
'Update available for `Kubb`',
|
|
124
|
+
{
|
|
125
|
+
width: 'auto',
|
|
126
|
+
formatBorder: pc.yellow,
|
|
127
|
+
rounded: true,
|
|
128
|
+
withGuide: false,
|
|
129
|
+
contentAlign: 'center',
|
|
130
|
+
titleAlign: 'center',
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
context.on('lifecycle:start', (version) => {
|
|
136
|
+
console.log(gradientString(['#F58517', '#F5A217', '#F55A17'])(`Kubb ${version} 🧩`))
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
context.on('config:start', () => {
|
|
140
|
+
if (logLevel <= LogLevel.silent) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const text = getMessage('Configuration started')
|
|
145
|
+
|
|
146
|
+
clack.intro(text)
|
|
147
|
+
startSpinner(getMessage('Configuration loading'))
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
context.on('config:end', () => {
|
|
151
|
+
if (logLevel <= LogLevel.silent) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const text = getMessage('Configuration completed')
|
|
156
|
+
|
|
157
|
+
clack.outro(text)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
context.on('generation:start', (config) => {
|
|
161
|
+
const text = getMessage(['Generation started', config.name ? `for ${pc.dim(config.name)}` : undefined].filter(Boolean).join(' '))
|
|
162
|
+
|
|
163
|
+
clack.intro(text)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
context.on('plugin:start', (plugin) => {
|
|
167
|
+
if (logLevel <= LogLevel.silent) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
stopSpinner()
|
|
172
|
+
|
|
173
|
+
const progressBar = clack.progress({
|
|
174
|
+
style: 'block',
|
|
175
|
+
max: 100,
|
|
176
|
+
size: 30,
|
|
177
|
+
})
|
|
178
|
+
const text = getMessage(`Generating ${pc.bold(plugin.name)}`)
|
|
179
|
+
progressBar.start(text)
|
|
180
|
+
|
|
181
|
+
const interval = setInterval(() => {
|
|
182
|
+
progressBar.advance()
|
|
183
|
+
}, 50)
|
|
184
|
+
|
|
185
|
+
activeProgress.set(plugin.name, { progressBar, interval })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
context.on('plugin:end', (plugin, duration) => {
|
|
189
|
+
stopSpinner()
|
|
190
|
+
|
|
191
|
+
const active = activeProgress.get(plugin.name)
|
|
192
|
+
|
|
193
|
+
if (!active || logLevel === LogLevel.silent) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
clearInterval(active.interval)
|
|
198
|
+
|
|
199
|
+
const durationStr = duration >= 1000 ? `${(duration / 1000).toFixed(2)}s` : `${duration}ms`
|
|
200
|
+
const text = getMessage(`${pc.bold(plugin.name)} completed in ${pc.green(durationStr)}`)
|
|
201
|
+
|
|
202
|
+
active.progressBar.stop(text)
|
|
203
|
+
activeProgress.delete(plugin.name)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
context.on('files:processing:start', (files) => {
|
|
207
|
+
if (logLevel <= LogLevel.silent) {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
stopSpinner()
|
|
212
|
+
|
|
213
|
+
const text = getMessage(`Writing ${files.length} files`)
|
|
214
|
+
const progressBar = clack.progress({
|
|
215
|
+
style: 'block',
|
|
216
|
+
max: files.length,
|
|
217
|
+
size: 30,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
context.emit('info', text)
|
|
221
|
+
progressBar.start(text)
|
|
222
|
+
activeProgress.set('files', { progressBar })
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
context.on('file:processing:update', ({ file, config }) => {
|
|
226
|
+
if (logLevel <= LogLevel.silent) {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
stopSpinner()
|
|
231
|
+
|
|
232
|
+
const text = `Writing ${relative(config.root, file.path)}`
|
|
233
|
+
const active = activeProgress.get('files')
|
|
234
|
+
|
|
235
|
+
if (!active) {
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
active.progressBar.advance(undefined, text)
|
|
240
|
+
})
|
|
241
|
+
context.on('files:processing:end', () => {
|
|
242
|
+
if (logLevel <= LogLevel.silent) {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
stopSpinner()
|
|
247
|
+
|
|
248
|
+
const text = getMessage('Files written successfully')
|
|
249
|
+
const active = activeProgress.get('files')
|
|
250
|
+
|
|
251
|
+
if (!active) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
active.progressBar.stop(text)
|
|
256
|
+
activeProgress.delete('files')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
context.on('generation:end', (config) => {
|
|
260
|
+
const text = getMessage(config.name ? `Generation completed for ${pc.dim(config.name)}` : 'Generation completed')
|
|
261
|
+
|
|
262
|
+
clack.outro(text)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
context.on('hook:execute', async ({ command, args }, cb) => {
|
|
266
|
+
if (logLevel <= LogLevel.silent) {
|
|
267
|
+
try {
|
|
268
|
+
const result = await execa(command, args, {
|
|
269
|
+
detached: true,
|
|
270
|
+
stripFinalNewline: true,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
await context.emit('debug', {
|
|
274
|
+
date: new Date(),
|
|
275
|
+
logs: [result.stdout],
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
cb()
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const error = new Error('Hook execute failed')
|
|
281
|
+
error.cause = err
|
|
282
|
+
|
|
283
|
+
await context.emit('debug', {
|
|
284
|
+
date: new Date(),
|
|
285
|
+
logs: [(err as any).stdout],
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
await context.emit('error', error)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const logger = clack.taskLog({
|
|
295
|
+
title: getMessage(['Executing hook', logLevel >= LogLevel.info ? pc.dim(`${command} ${args?.join(' ')}`) : undefined].filter(Boolean).join(' ')),
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const writable = new ClackWritable(logger)
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const result = await execa(command, args, {
|
|
302
|
+
detached: true,
|
|
303
|
+
stdout: ['pipe', writable],
|
|
304
|
+
stripFinalNewline: true,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
await context.emit('debug', {
|
|
308
|
+
date: new Date(),
|
|
309
|
+
logs: [result.stdout],
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
cb()
|
|
313
|
+
} catch (err) {
|
|
314
|
+
const error = new Error('Hook execute failed')
|
|
315
|
+
error.cause = err
|
|
316
|
+
|
|
317
|
+
await context.emit('debug', {
|
|
318
|
+
date: new Date(),
|
|
319
|
+
logs: [(err as any).stdout],
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
await context.emit('error', error)
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
context.on('format:start', () => {
|
|
327
|
+
if (logLevel <= LogLevel.silent) {
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const text = getMessage('Format started')
|
|
332
|
+
|
|
333
|
+
clack.intro(text)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
context.on('format:end', () => {
|
|
337
|
+
if (logLevel <= LogLevel.silent) {
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const text = getMessage('Format completed')
|
|
342
|
+
|
|
343
|
+
clack.outro(text)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
context.on('lint:start', () => {
|
|
347
|
+
if (logLevel <= LogLevel.silent) {
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const text = getMessage('Lint started')
|
|
352
|
+
|
|
353
|
+
clack.intro(text)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
context.on('lint:end', () => {
|
|
357
|
+
if (logLevel <= LogLevel.silent) {
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const text = getMessage('Lint completed')
|
|
362
|
+
|
|
363
|
+
clack.outro(text)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
context.on('hook:start', (command) => {
|
|
367
|
+
if (logLevel <= LogLevel.silent) {
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const text = getMessage(`Hook ${pc.dim(command)} started`)
|
|
372
|
+
|
|
373
|
+
clack.intro(text)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
context.on('hook:end', (command) => {
|
|
377
|
+
if (logLevel <= LogLevel.silent) {
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const text = getMessage(`Hook ${pc.dim(command)} completed`)
|
|
382
|
+
|
|
383
|
+
clack.outro(text)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
context.on('generation:summary', (config, { pluginTimings, failedPlugins, filesCreated, status, hrStart }) => {
|
|
387
|
+
const summary = getSummary({
|
|
388
|
+
failedPlugins,
|
|
389
|
+
filesCreated,
|
|
390
|
+
config,
|
|
391
|
+
status,
|
|
392
|
+
hrStart,
|
|
393
|
+
pluginTimings: logLevel >= LogLevel.verbose ? pluginTimings : undefined,
|
|
394
|
+
})
|
|
395
|
+
const title = config.name || ''
|
|
396
|
+
|
|
397
|
+
summary.unshift('\n')
|
|
398
|
+
summary.push('\n')
|
|
399
|
+
|
|
400
|
+
if (status === 'success') {
|
|
401
|
+
clack.box(summary.join('\n'), getMessage(title), {
|
|
402
|
+
width: 'auto',
|
|
403
|
+
formatBorder: pc.green,
|
|
404
|
+
rounded: true,
|
|
405
|
+
withGuide: false,
|
|
406
|
+
contentAlign: 'left',
|
|
407
|
+
titleAlign: 'center',
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
clack.box(summary.join('\n'), getMessage(title), {
|
|
414
|
+
width: 'auto',
|
|
415
|
+
formatBorder: pc.red,
|
|
416
|
+
rounded: true,
|
|
417
|
+
withGuide: false,
|
|
418
|
+
contentAlign: 'left',
|
|
419
|
+
titleAlign: 'center',
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
context.on('lifecycle:end', () => {
|
|
424
|
+
for (const [_key, active] of activeProgress) {
|
|
425
|
+
if (active.interval) {
|
|
426
|
+
clearInterval(active.interval)
|
|
427
|
+
}
|
|
428
|
+
active.progressBar?.stop()
|
|
429
|
+
}
|
|
430
|
+
activeProgress.clear()
|
|
431
|
+
})
|
|
432
|
+
},
|
|
433
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if running in GitHub Actions environment
|
|
3
|
+
*/
|
|
4
|
+
export function isGitHubActions(): boolean {
|
|
5
|
+
return !!process.env.GITHUB_ACTIONS
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if running in any CI environment
|
|
10
|
+
*/
|
|
11
|
+
export function isCIEnvironment(): boolean {
|
|
12
|
+
return !!(
|
|
13
|
+
process.env.CI ||
|
|
14
|
+
process.env.GITHUB_ACTIONS ||
|
|
15
|
+
process.env.GITLAB_CI ||
|
|
16
|
+
process.env.CIRCLECI ||
|
|
17
|
+
process.env.TRAVIS ||
|
|
18
|
+
process.env.JENKINS_URL ||
|
|
19
|
+
process.env.BUILDKITE
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if TTY is available for interactive output
|
|
25
|
+
*/
|
|
26
|
+
export function canUseTTY(): boolean {
|
|
27
|
+
return !!process.stdout.isTTY && !isCIEnvironment()
|
|
28
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { defineLogger } from '@kubb/core'
|
|
3
|
+
import { write } from '@kubb/core/fs'
|
|
4
|
+
|
|
5
|
+
type CachedEvent = {
|
|
6
|
+
date: Date
|
|
7
|
+
logs: string[]
|
|
8
|
+
fileName?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* FileSystem logger for debug log persistence
|
|
13
|
+
* Captures debug and verbose events and writes them to files in .kubb directory
|
|
14
|
+
*
|
|
15
|
+
* Note: Logs are written on lifecycle:end or process exit. If the process crashes
|
|
16
|
+
* before these events, some cached logs may be lost.
|
|
17
|
+
*/
|
|
18
|
+
export const fileSystemLogger = defineLogger({
|
|
19
|
+
name: 'filesystem',
|
|
20
|
+
install(context) {
|
|
21
|
+
const cachedLogs: Set<CachedEvent> = new Set()
|
|
22
|
+
const startDate = Date.now()
|
|
23
|
+
|
|
24
|
+
async function writeLogs() {
|
|
25
|
+
if (cachedLogs.size === 0) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const files: Record<string, string[]> = {}
|
|
30
|
+
|
|
31
|
+
for (const log of cachedLogs) {
|
|
32
|
+
const fileName = resolve(process.cwd(), '.kubb', log.fileName || `kubb-${startDate}.log`)
|
|
33
|
+
|
|
34
|
+
if (!files[fileName]) {
|
|
35
|
+
files[fileName] = []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (log.logs.length > 0) {
|
|
39
|
+
const timestamp = log.date.toLocaleString()
|
|
40
|
+
files[fileName].push(`[${timestamp}]\n${log.logs.join('\n')}`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await Promise.all(
|
|
45
|
+
Object.entries(files).map(async ([fileName, logs]) => {
|
|
46
|
+
return write(fileName, logs.join('\n\n'))
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
cachedLogs.clear()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
context.on('debug', (message) => {
|
|
54
|
+
cachedLogs.add({
|
|
55
|
+
date: new Date(),
|
|
56
|
+
logs: message.logs,
|
|
57
|
+
fileName: undefined,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
context.on('lifecycle:end', async () => {
|
|
62
|
+
await writeLogs()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Fallback: Write logs on process exit to handle crashes
|
|
66
|
+
const exitHandler = () => {
|
|
67
|
+
// Synchronous write on exit - best effort
|
|
68
|
+
if (cachedLogs.size > 0) {
|
|
69
|
+
writeLogs().catch(() => {
|
|
70
|
+
// Ignore errors on exit
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.once('exit', exitHandler)
|
|
76
|
+
process.once('SIGINT', exitHandler)
|
|
77
|
+
process.once('SIGTERM', exitHandler)
|
|
78
|
+
},
|
|
79
|
+
})
|