codebuddy-stats 1.1.0 → 1.1.2

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/src/index.ts DELETED
@@ -1,615 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'node:fs'
4
- import path from 'node:path'
5
- import { fileURLToPath } from 'node:url'
6
- import blessed from 'blessed'
7
-
8
- import { loadUsageData } from './lib/data-loader.js'
9
- import type { AnalysisData } from './lib/data-loader.js'
10
- import { resolveProjectName } from './lib/workspace-resolver.js'
11
- import { formatCost, formatNumber, formatPercent, formatTokens, truncate } from './lib/utils.js'
12
-
13
- // 读取 package.json 获取版本号
14
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
15
- const pkgPath = path.resolve(__dirname, '../package.json')
16
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version: string }
17
- const VERSION = pkg.version
18
-
19
- type CliOptions = {
20
- days: number | null
21
- noTui: boolean
22
- }
23
-
24
- // 解析命令行参数
25
- function parseArgs(): CliOptions {
26
- const args = process.argv.slice(2)
27
- const options: CliOptions = { days: null, noTui: false }
28
-
29
- for (let i = 0; i < args.length; i++) {
30
- if (args[i] === '--days' && args[i + 1]) {
31
- const parsed = Number.parseInt(args[i + 1]!, 10)
32
- options.days = Number.isFinite(parsed) ? parsed : null
33
- i++
34
- } else if (args[i] === '--no-tui') {
35
- options.noTui = true
36
- } else if (args[i] === '--help' || args[i] === '-h') {
37
- console.log(`
38
- CodeBuddy Cost Analyzer
39
-
40
- Usage: cost-analyzer [options]
41
-
42
- Options:
43
- --days <n> 只显示最近 n 天的数据
44
- --no-tui 使用纯文本输出(不启用交互式界面)
45
- --help, -h 显示帮助信息
46
- `)
47
- process.exit(0)
48
- }
49
- }
50
- return options
51
- }
52
-
53
- type HeatmapData = {
54
- dates: string[]
55
- costs: number[]
56
- maxCost: number
57
- }
58
-
59
- // 生成热力图数据
60
- function generateHeatmapData(dailySummary: AnalysisData['dailySummary']): HeatmapData {
61
- const sortedDates = Object.keys(dailySummary).sort()
62
- if (sortedDates.length === 0) return { dates: [], costs: [], maxCost: 0 }
63
-
64
- const costs = sortedDates.map(d => dailySummary[d]?.cost ?? 0)
65
- const maxCost = Math.max(...costs)
66
-
67
- return {
68
- dates: sortedDates,
69
- costs,
70
- maxCost,
71
- }
72
- }
73
-
74
- // 获取热力图字符
75
- function getHeatChar(cost: number, maxCost: number): string {
76
- if (cost === 0) return '·'
77
- const ratio = cost / maxCost
78
- if (ratio < 0.25) return '░'
79
- if (ratio < 0.5) return '▒'
80
- if (ratio < 0.75) return '▓'
81
- return '█'
82
- }
83
-
84
- // 渲染 Overview 视图
85
- function renderOverview(box: any, data: AnalysisData, width: number, note: string): void {
86
- const { dailySummary, grandTotal, topModel, topProject, cacheHitRate, activeDays } = data
87
- const heatmap = generateHeatmapData(dailySummary)
88
-
89
- // 根据宽度计算热力图周数
90
- const availableWidth = width - 10
91
- const maxWeeks = Math.min(Math.floor(availableWidth / 2), 26) // 最多 26 周 (半年)
92
-
93
- let content = '{bold}Cost Heatmap{/bold}\n\n'
94
-
95
- // 生成正确的日期网格 - 从今天往前推算
96
- const today = new Date()
97
- const todayStr = today.toISOString().split('T')[0]!
98
-
99
- // 找到最近的周六作为结束点(或今天)
100
- const endDate = new Date(today)
101
-
102
- // 往前推 maxWeeks 周
103
- const startDate = new Date(endDate)
104
- startDate.setDate(startDate.getDate() - maxWeeks * 7 + 1)
105
- // 调整到周日开始
106
- startDate.setDate(startDate.getDate() - startDate.getDay())
107
-
108
- // 构建周数组,每周从周日到周六
109
- const weeks: string[][] = []
110
- const currentDate = new Date(startDate)
111
- while (currentDate <= endDate) {
112
- const week: string[] = []
113
- for (let d = 0; d < 7; d++) {
114
- const dateStr = currentDate.toISOString().split('T')[0]!
115
- week.push(dateStr)
116
- currentDate.setDate(currentDate.getDate() + 1)
117
- }
118
- weeks.push(week)
119
- }
120
-
121
- const maxCost = heatmap.maxCost || 1
122
- const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
123
-
124
- for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
125
- let row = dayLabels[dayOfWeek]!.padEnd(4)
126
- for (const week of weeks) {
127
- const date = week[dayOfWeek]
128
- if (date && date <= todayStr && dailySummary[date]) {
129
- row += getHeatChar(dailySummary[date]!.cost, maxCost) + ' '
130
- } else if (date && date <= todayStr) {
131
- row += '· ' // 有日期但无数据
132
- } else {
133
- row += ' ' // 未来日期
134
- }
135
- }
136
- content += row + '\n'
137
- }
138
-
139
- content += ' Less {gray-fg}·░▒▓{/gray-fg}{white-fg}█{/white-fg} More\n\n'
140
-
141
- // 汇总指标 - 根据宽度决定布局
142
- const avgDailyCost = activeDays > 0 ? grandTotal.cost / activeDays : 0
143
- const summaryWidth = Math.min(width - 6, 70)
144
-
145
- content += '{bold}Summary{/bold}\n'
146
- content += '─'.repeat(summaryWidth) + '\n'
147
-
148
- if (width >= 80) {
149
- // 双列布局
150
- content += `{green-fg}Total cost:{/green-fg} ${formatCost(grandTotal.cost).padStart(12)} `
151
- content += `{green-fg}Active days:{/green-fg} ${String(activeDays).padStart(8)}\n`
152
- content += `{green-fg}Total tokens:{/green-fg} ${formatTokens(grandTotal.tokens).padStart(12)} `
153
- content += `{green-fg}Total requests:{/green-fg} ${formatNumber(grandTotal.requests).padStart(8)}\n`
154
- content += `{green-fg}Cache hit rate:{/green-fg} ${formatPercent(cacheHitRate).padStart(12)} `
155
- content += `{green-fg}Avg daily cost:{/green-fg} ${formatCost(avgDailyCost).padStart(8)}\n\n`
156
- } else {
157
- // 单列布局
158
- content += `{green-fg}Total cost:{/green-fg} ${formatCost(grandTotal.cost)}\n`
159
- content += `{green-fg}Total tokens:{/green-fg} ${formatTokens(grandTotal.tokens)}\n`
160
- content += `{green-fg}Total requests:{/green-fg} ${formatNumber(grandTotal.requests)}\n`
161
- content += `{green-fg}Active days:{/green-fg} ${activeDays}\n`
162
- content += `{green-fg}Cache hit rate:{/green-fg} ${formatPercent(cacheHitRate)}\n`
163
- content += `{green-fg}Avg daily cost:{/green-fg} ${formatCost(avgDailyCost)}\n\n`
164
- }
165
-
166
- if (topModel) {
167
- content += `{cyan-fg}Top model:{/cyan-fg} ${topModel.id} (${formatCost(topModel.cost)})\n`
168
- }
169
- if (topProject) {
170
- const projectMaxLen = width >= 100 ? 60 : 35
171
- const shortName = resolveProjectName(topProject.name, data.workspaceMappings)
172
- content += `{cyan-fg}Top project:{/cyan-fg} ${truncate(shortName, projectMaxLen)} (${formatCost(topProject.cost)})\n`
173
- }
174
-
175
- if (note) {
176
- content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
177
- }
178
-
179
- box.setContent(content)
180
- }
181
-
182
- // 渲染 By Model 视图
183
- function renderByModel(box: any, data: AnalysisData, width: number, note: string): void {
184
- const { modelTotals, grandTotal } = data
185
- const sorted = Object.entries(modelTotals).sort((a, b) => b[1].cost - a[1].cost)
186
-
187
- // 根据宽度计算列宽
188
- const availableWidth = width - 6 // padding
189
- const fixedCols = 12 + 12 + 12 + 10 // Cost + Requests + Tokens + Avg/Req
190
- const modelCol = Math.max(20, Math.min(40, availableWidth - fixedCols))
191
- const totalWidth = modelCol + fixedCols
192
-
193
- let content = '{bold}Cost by Model{/bold}\n\n'
194
- content +=
195
- '{underline}' +
196
- 'Model'.padEnd(modelCol) +
197
- 'Cost'.padStart(12) +
198
- 'Requests'.padStart(12) +
199
- 'Tokens'.padStart(12) +
200
- 'Avg/Req'.padStart(10) +
201
- '{/underline}\n'
202
-
203
- for (const [modelId, stats] of sorted) {
204
- const avgPerReq = stats.requests > 0 ? stats.cost / stats.requests : 0
205
- content +=
206
- truncate(modelId, modelCol - 1).padEnd(modelCol) +
207
- formatCost(stats.cost).padStart(12) +
208
- formatNumber(stats.requests).padStart(12) +
209
- formatTokens(stats.tokens).padStart(12) +
210
- formatCost(avgPerReq).padStart(10) +
211
- '\n'
212
- }
213
-
214
- content += '─'.repeat(totalWidth) + '\n'
215
- content +=
216
- '{bold}' +
217
- 'Total'.padEnd(modelCol) +
218
- formatCost(grandTotal.cost).padStart(12) +
219
- formatNumber(grandTotal.requests).padStart(12) +
220
- formatTokens(grandTotal.tokens).padStart(12) +
221
- '{/bold}\n'
222
-
223
- if (note) {
224
- content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
225
- }
226
-
227
- box.setContent(content)
228
- }
229
-
230
- // 渲染 By Project 视图
231
- function renderByProject(box: any, data: AnalysisData, width: number, note: string): void {
232
- const { projectTotals, grandTotal } = data
233
- const sorted = Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost)
234
-
235
- // 根据宽度计算列宽
236
- const availableWidth = width - 6 // padding
237
- const fixedCols = 12 + 12 + 12 // Cost + Requests + Tokens
238
- const projectCol = Math.max(25, availableWidth - fixedCols)
239
- const totalWidth = projectCol + fixedCols
240
-
241
- let content = '{bold}Cost by Project{/bold}\n\n'
242
- content +=
243
- '{underline}' +
244
- 'Project'.padEnd(projectCol) +
245
- 'Cost'.padStart(12) +
246
- 'Requests'.padStart(12) +
247
- 'Tokens'.padStart(12) +
248
- '{/underline}\n'
249
-
250
- for (const [projectName, stats] of sorted) {
251
- // 简化项目名
252
- const shortName = resolveProjectName(projectName, data.workspaceMappings)
253
- content +=
254
- truncate(shortName, projectCol - 1).padEnd(projectCol) +
255
- formatCost(stats.cost).padStart(12) +
256
- formatNumber(stats.requests).padStart(12) +
257
- formatTokens(stats.tokens).padStart(12) +
258
- '\n'
259
- }
260
-
261
- content += '─'.repeat(totalWidth) + '\n'
262
- content +=
263
- '{bold}' +
264
- `Total (${sorted.length} projects)`.padEnd(projectCol) +
265
- formatCost(grandTotal.cost).padStart(12) +
266
- formatNumber(grandTotal.requests).padStart(12) +
267
- formatTokens(grandTotal.tokens).padStart(12) +
268
- '{/bold}\n'
269
-
270
- if (note) {
271
- content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
272
- }
273
-
274
- box.setContent(content)
275
- }
276
-
277
- // 渲染 Daily 视图
278
- function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: number, note: string): void {
279
- const { dailySummary, dailyData } = data
280
- const sortedDates = Object.keys(dailySummary).sort().reverse()
281
-
282
- // 根据宽度计算列宽
283
- const availableWidth = width - 6 // padding
284
- const dateCol = 12
285
- const costCol = 12
286
- const reqCol = 10
287
- const fixedCols = dateCol + costCol + reqCol
288
- const remainingWidth = availableWidth - fixedCols
289
- const modelCol = Math.max(15, Math.min(25, Math.floor(remainingWidth * 0.4)))
290
- const projectCol = Math.max(20, remainingWidth - modelCol)
291
-
292
- let content = '{bold}Daily Cost Details{/bold}\n\n'
293
- content +=
294
- '{underline}' +
295
- 'Date'.padEnd(dateCol) +
296
- 'Cost'.padStart(costCol) +
297
- 'Requests'.padStart(reqCol) +
298
- 'Top Model'.padStart(modelCol) +
299
- 'Top Project'.padStart(projectCol) +
300
- '{/underline}\n'
301
-
302
- const visibleDates = sortedDates.slice(scrollOffset, scrollOffset + 20)
303
-
304
- for (const date of visibleDates) {
305
- const daySummary = dailySummary[date]
306
- const dayData = dailyData[date]
307
- if (!daySummary || !dayData) continue
308
-
309
- // 找出当天 top model 和 project
310
- let topModel: { id: string; cost: number } = { id: '-', cost: 0 }
311
- let topProject: { name: string; cost: number } = { name: '-', cost: 0 }
312
-
313
- for (const [project, models] of Object.entries(dayData)) {
314
- let projectCost = 0
315
- for (const [model, stats] of Object.entries(models)) {
316
- const modelStats = stats as any
317
- projectCost += Number(modelStats.cost ?? 0)
318
- if (Number(modelStats.cost ?? 0) > topModel.cost) {
319
- topModel = { id: model, cost: Number(modelStats.cost ?? 0) }
320
- }
321
- }
322
- if (projectCost > topProject.cost) {
323
- topProject = { name: project, cost: projectCost }
324
- }
325
- }
326
-
327
- const shortProject = resolveProjectName(topProject.name, data.workspaceMappings)
328
-
329
- content +=
330
- date.padEnd(dateCol) +
331
- formatCost(daySummary.cost).padStart(costCol) +
332
- formatNumber(daySummary.requests).padStart(reqCol) +
333
- truncate(topModel.id, modelCol - 1).padStart(modelCol) +
334
- truncate(shortProject, projectCol - 1).padStart(projectCol) +
335
- '\n'
336
- }
337
-
338
- if (sortedDates.length > 20) {
339
- content += `\n{gray-fg}Showing ${scrollOffset + 1}-${Math.min(scrollOffset + 20, sortedDates.length)} of ${sortedDates.length} days (↑↓ to scroll){/gray-fg}`
340
- }
341
-
342
- if (note) {
343
- content += `\n\n{gray-fg}备注:${note}{/gray-fg}\n`
344
- }
345
-
346
- box.setContent(content)
347
- }
348
-
349
- // 纯文本输出模式
350
- function printTextReport(data: AnalysisData): void {
351
- const { modelTotals, projectTotals, grandTotal, topModel, topProject, cacheHitRate, activeDays } = data
352
-
353
- console.log('\n🤖 CodeBuddy Cost Analysis Report')
354
- console.log('='.repeat(50))
355
-
356
- console.log(`\nTotal cost: ${formatCost(grandTotal.cost)}`)
357
- console.log(`Total tokens: ${formatTokens(grandTotal.tokens)}`)
358
- console.log(`Total requests: ${formatNumber(grandTotal.requests)}`)
359
- console.log(`Active days: ${activeDays}`)
360
- console.log(`Cache hit rate: ${formatPercent(cacheHitRate)}`)
361
-
362
- if (topModel) {
363
- console.log(`\nTop model: ${topModel.id} (${formatCost(topModel.cost)})`)
364
- }
365
- if (topProject) {
366
- const shortName = resolveProjectName(topProject.name, data.workspaceMappings)
367
- console.log(`Top project: ${shortName}`)
368
- console.log(` (${formatCost(topProject.cost)})`)
369
- }
370
-
371
- console.log('\n' + '-'.repeat(50))
372
- console.log('By Model:')
373
- for (const [model, stats] of Object.entries(modelTotals).sort((a, b) => b[1].cost - a[1].cost)) {
374
- console.log(` ${model}: ${formatCost(stats.cost)} (${formatNumber(stats.requests)} req)`) // eslint-disable-line no-console
375
- }
376
-
377
- console.log('\n' + '-'.repeat(50))
378
- console.log('By Project:')
379
- for (const [project, stats] of Object.entries(projectTotals)
380
- .sort((a, b) => b[1].cost - a[1].cost)
381
- .slice(0, 10)) {
382
- const shortName = resolveProjectName(project, data.workspaceMappings)
383
- console.log(` ${truncate(shortName, 40)}: ${formatCost(stats.cost)}`) // eslint-disable-line no-console
384
- }
385
-
386
- console.log('\n' + '='.repeat(50) + '\n')
387
- }
388
-
389
- // 主程序
390
- async function main(): Promise<void> {
391
- const options = parseArgs()
392
-
393
- console.log('Loading data...')
394
- let currentSource: 'code' | 'ide' = 'code'
395
- let data = await loadUsageData({ days: options.days, source: currentSource })
396
-
397
- if (options.noTui) {
398
- printTextReport(data)
399
- return
400
- }
401
-
402
- // 创建 TUI
403
- const screen = blessed.screen({
404
- smartCSR: true,
405
- title: 'CodeBuddy Cost Analyzer',
406
- forceUnicode: true,
407
- fullUnicode: true,
408
- })
409
-
410
- // Tab 状态
411
- const tabs = ['Overview', 'By Model', 'By Project', 'Daily']
412
- let currentTab = 0
413
- let dailyScrollOffset = 0
414
-
415
- // Tab 栏
416
- const tabBar = blessed.box({
417
- top: 0,
418
- left: 0,
419
- width: '100%',
420
- height: 3,
421
- tags: true,
422
- style: {
423
- fg: 'white',
424
- bg: 'black',
425
- },
426
- })
427
-
428
- // 内容区域
429
- const contentBox = blessed.box({
430
- top: 3,
431
- left: 0,
432
- width: '100%',
433
- height: '100%-5',
434
- tags: true,
435
- scrollable: true,
436
- alwaysScroll: true,
437
- keys: true,
438
- vi: true,
439
- style: {
440
- fg: 'white',
441
- bg: 'black',
442
- },
443
- padding: {
444
- left: 2,
445
- right: 2,
446
- top: 1,
447
- },
448
- })
449
-
450
- // 底部状态栏
451
- const statusBar = blessed.box({
452
- bottom: 0,
453
- left: 0,
454
- width: '100%',
455
- height: 1,
456
- tags: true,
457
- style: {
458
- fg: 'black',
459
- bg: 'green',
460
- },
461
- })
462
-
463
- screen.append(tabBar)
464
- screen.append(contentBox)
465
- screen.append(statusBar)
466
-
467
- // 更新 Tab 栏
468
- function updateTabBar(): void {
469
- let content = ' Cost Analysis '
470
-
471
- content += '{gray-fg}Source:{/gray-fg} '
472
- if (currentSource === 'code') {
473
- content += '{black-fg}{green-bg} Code {/green-bg}{/black-fg} '
474
- content += '{gray-fg}IDE{/gray-fg} '
475
- } else {
476
- content += '{gray-fg}Code{/gray-fg} '
477
- content += '{black-fg}{green-bg} IDE {/green-bg}{/black-fg} '
478
- }
479
-
480
- content += '{gray-fg}Views:{/gray-fg} '
481
-
482
- for (let i = 0; i < tabs.length; i++) {
483
- if (i === currentTab) {
484
- content += `{black-fg}{green-bg} ${tabs[i]} {/green-bg}{/black-fg} `
485
- } else {
486
- content += `{gray-fg}${tabs[i]}{/gray-fg} `
487
- }
488
- }
489
- content += ' {gray-fg}(Tab view, s source){/gray-fg}'
490
- tabBar.setContent(content)
491
- }
492
-
493
- // 更新内容
494
- function updateContent(): void {
495
- const width = Number(screen.width) || 80
496
-
497
- const note =
498
- currentSource === 'code'
499
- ? `针对 CodeBuddy Code ≤ 2.20.0 版本产生的数据,由于没有请求级别的 model ID,用量是基于当前 CodeBuddy Code 设置的 model ID(${data.defaultModelId})计算价格的`
500
- : 'IDE 的 usage 不包含缓存命中/写入 tokens,无法计算缓存相关价格与命中率;成本按 input/output tokens 估算'
501
-
502
- switch (currentTab) {
503
- case 0:
504
- renderOverview(contentBox, data, width, note)
505
- break
506
- case 1:
507
- renderByModel(contentBox, data, width, note)
508
- break
509
- case 2:
510
- renderByProject(contentBox, data, width, note)
511
- break
512
- case 3:
513
- renderDaily(contentBox, data, dailyScrollOffset, width, note)
514
- break
515
- }
516
- }
517
-
518
- // 更新状态栏
519
- function updateStatusBar(): void {
520
- const daysInfo = options.days ? `Last ${options.days} days` : 'All time'
521
- const sourceInfo = currentSource === 'code' ? 'Code' : 'IDE'
522
- const leftContent = ` ${daysInfo} | Source: ${sourceInfo} | Total: ${formatCost(data.grandTotal.cost)} | q quit, Tab view, s source, r refresh`
523
- const rightContent = `v${VERSION} `
524
- const width = Number(screen.width) || 80
525
- const padding = Math.max(0, width - leftContent.length - rightContent.length)
526
- statusBar.setContent(leftContent + ' '.repeat(padding) + rightContent)
527
- }
528
-
529
- // 键盘事件
530
- screen.key(['tab'], () => {
531
- currentTab = (currentTab + 1) % tabs.length
532
- dailyScrollOffset = 0
533
- updateTabBar()
534
- updateContent()
535
- screen.render()
536
- })
537
-
538
- screen.key(['S-tab'], () => {
539
- currentTab = (currentTab - 1 + tabs.length) % tabs.length
540
- dailyScrollOffset = 0
541
- updateTabBar()
542
- updateContent()
543
- screen.render()
544
- })
545
-
546
- screen.key(['up', 'k'], () => {
547
- if (currentTab === 3) {
548
- dailyScrollOffset = Math.max(0, dailyScrollOffset - 1)
549
- updateContent()
550
- screen.render()
551
- }
552
- })
553
-
554
- screen.key(['down', 'j'], () => {
555
- if (currentTab === 3) {
556
- const maxOffset = Math.max(0, Object.keys(data.dailySummary).length - 20)
557
- dailyScrollOffset = Math.min(maxOffset, dailyScrollOffset + 1)
558
- updateContent()
559
- screen.render()
560
- }
561
- })
562
-
563
- screen.key(['q', 'C-c'], () => {
564
- screen.destroy()
565
- process.exit(0)
566
- })
567
-
568
- screen.key(['r'], async () => {
569
- statusBar.setContent(' {yellow-fg}Reloading...{/yellow-fg}')
570
- screen.render()
571
- try {
572
- data = await loadUsageData({ days: options.days, source: currentSource })
573
- dailyScrollOffset = 0
574
- updateTabBar()
575
- updateContent()
576
- updateStatusBar()
577
- } catch (err) {
578
- statusBar.setContent(` {red-fg}Reload failed: ${String(err)}{/red-fg}`)
579
- }
580
- screen.render()
581
- })
582
-
583
- screen.key(['s'], async () => {
584
- statusBar.setContent(' {yellow-fg}Switching source...{/yellow-fg}')
585
- screen.render()
586
- try {
587
- currentSource = currentSource === 'code' ? 'ide' : 'code'
588
- data = await loadUsageData({ days: options.days, source: currentSource })
589
- dailyScrollOffset = 0
590
- updateTabBar()
591
- updateContent()
592
- updateStatusBar()
593
- } catch (err) {
594
- statusBar.setContent(` {red-fg}Switch source failed: ${String(err)}{/red-fg}`)
595
- }
596
- screen.render()
597
- })
598
-
599
- // 监听窗口大小变化
600
- screen.on('resize', () => {
601
- updateContent()
602
- screen.render()
603
- })
604
-
605
- // 初始渲染
606
- updateTabBar()
607
- updateContent()
608
- updateStatusBar()
609
- screen.render()
610
- }
611
-
612
- main().catch(err => {
613
- console.error('Error:', err)
614
- process.exit(1)
615
- })