wukong-gitlog-cli 1.0.37 → 1.0.38

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/.eslintrc CHANGED
@@ -24,6 +24,7 @@
24
24
  "consistent-return": "off",
25
25
  "no-await-in-loop": "warn",
26
26
  "no-unused-vars": "warn",
27
+ "no-nested-ternary": "warn",
27
28
  "no-console": "off",
28
29
  "no-plusplus": "off",
29
30
  "no-unresolved": "off",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.0.38](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.37...v1.0.38) (2025-12-27)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 增加性能分析 ([ec925cc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/ec925cc3d20be2a0eee6e20aaee99b4449eff9be))
11
+ * 🎸 flame 和 显示 flame-like 日志 ([7b1a00c](https://github.com/tomatobybike/wukong-gitlog-cli/commit/7b1a00c26afa1757637e6c4a3a2be92d74e0ad65))
12
+ * 🎸 ignore ([eb3e5bc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/eb3e5bc89183e0722056036f14f74806e3c3b1e3))
13
+ * 🎸 profiler ([48fd0a5](https://github.com/tomatobybike/wukong-gitlog-cli/commit/48fd0a5a8b25b55bbca7bbdbd2df11158049adcd))
14
+ * 🎸 Profiler ([a7f4623](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a7f4623a162947d31f25702052f54a4ff0406fcb))
15
+
5
16
  ### [1.0.37](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.36...v1.0.37) (2025-12-12)
6
17
 
7
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.37",
3
+ "version": "1.0.38",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
@@ -37,40 +37,42 @@
37
37
  "wukong-gitlog-cli": "./bin/wukong-gitlog-cli"
38
38
  },
39
39
  "scripts": {
40
+ "build": "node scripts/esbuild.config.mjs",
41
+ "check:npm:login": "node scripts/check-npm-login.mjs",
40
42
  "cli:excel-demo": "node ./src/cli.mjs --overtime --format excel --stats --limit 5 --out commits.xlsx",
41
43
  "cli:excel-demo-parent": "node ./src/cli.mjs --out-parent --format excel --stats --limit 5 --out commits-parent.xlsx",
42
44
  "cli:gerrit-changeid-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{changeId}}\" --limit 5 --out commits-gerrit-changeid.txt",
43
45
  "cli:gerrit-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{hash}}\" --limit 5 --out commits-gerrit.txt",
44
46
  "cli:help": "node ./src/cli.mjs --help",
45
47
  "cli:json-demo": "node ./src/cli.mjs --json --limit 5 --out commits.json",
46
- "cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
47
- "cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
48
48
  "cli:overtime-demo": "node ./src/cli.mjs --overtime --format text --limit 200",
49
- "cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
50
49
  "cli:overtime-excel": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx",
51
- "cli:overtime-text-us": "node ./src/cli.mjs --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
52
- "cli:overtime-text-us-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
53
- "cli:overtime-text-us-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
54
50
  "cli:overtime-excel-cn": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx --country CN",
55
- "cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
56
51
  "cli:overtime-excel-cn-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format excel --out commits.xlsx --country CN",
52
+ "cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
57
53
  "cli:overtime-per-period-csv-tab": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab",
58
- "cli:overtime-per-period-xlsx-sheets": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets",
59
- "cli:overtime-per-period-xlsx-files": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files",
60
54
  "cli:overtime-per-period-only": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-only",
55
+ "cli:overtime-per-period-xlsx-files": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files",
56
+ "cli:overtime-per-period-xlsx-sheets": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets",
57
+ "cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
58
+ "cli:overtime-text-us": "node ./src/cli.mjs --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
59
+ "cli:overtime-text-us-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
60
+ "cli:overtime-text-us-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
61
61
  "cli:serve": "node ./src/cli.mjs --serve --overtime --format text --out commits.txt",
62
62
  "cli:serve-only": "node ./src/cli.mjs --serve-only",
63
+ "cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
64
+ "cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
63
65
  "format": "prettier --write \"src/**/*.{js,mjs}\"",
64
66
  "lint": "eslint src --ext .js,.mjs src",
65
67
  "lint:fix": "eslint src --ext .js,.mjs --fix",
66
68
  "pack": "npm pack",
67
- "build": "node scripts/esbuild.config.mjs",
68
69
  "prepare": "husky install",
69
- "prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
70
70
  "prepublishOnly": "yarn lint && yarn build",
71
- "check:npm:login": "node scripts/check-npm-login.mjs",
72
- "release": "yarn check:npm:login && yarn release:patch && yarn push:tags && npm publish",
71
+ "prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
73
72
  "push:tags": "git push origin main --follow-tags",
73
+ "prerelease": "yarn lint",
74
+ "release": "yarn check:npm:login && yarn release:patch && npm publish && yarn push:tags",
75
+ "release:rollback": "git reset --hard HEAD~1 && git tag -d $(git tag --points-at HEAD~1)",
74
76
  "release:major": "yarn lint && standard-version --release-as major",
75
77
  "release:minor": "yarn lint && standard-version --release-as minor",
76
78
  "release:patch": "yarn lint && standard-version --release-as patch",
@@ -94,15 +96,16 @@
94
96
  "boxen": "8.0.1",
95
97
  "chalk": "5.6.2",
96
98
  "commander": "12.1.0",
99
+ "date-fns": "4.1.0",
97
100
  "date-holidays": "2.1.1",
98
101
  "dayjs": "1.11.19",
99
- "date-fns": "4.1.0",
100
102
  "dotenv": "17.2.2",
101
103
  "exceljs": "4.4.0",
102
104
  "is-online": "12.0.2",
103
105
  "ora": "9.0.0",
104
106
  "semver": "6.3.1",
105
107
  "string-width": "5.1.2",
108
+ "wukong-profiler": "^1.0.5",
106
109
  "zx": "7.2.4"
107
110
  },
108
111
  "devDependencies": {
package/src/index.mjs CHANGED
@@ -6,6 +6,7 @@ import fs from 'fs'
6
6
  import ora from 'ora'
7
7
  import path from 'path'
8
8
  import { fileURLToPath } from 'url'
9
+ import { createProfiler } from 'wukong-profiler'
9
10
 
10
11
  import { parseOptions } from './cli/parseOptions.mjs'
11
12
  // eslint-disable-next-line no-unused-vars
@@ -50,6 +51,8 @@ dayjs.extend(isoWeek)
50
51
  const PKG_NAME = pkg.name
51
52
  const VERSION = pkg.version
52
53
 
54
+ let profiler
55
+
53
56
  const autoCheckUpdate = async () => {
54
57
  // === CLI 主逻辑完成后提示更新 ===
55
58
  await checkUpdateWithPatch({
@@ -83,8 +86,6 @@ export function getWeekRange(periodStr) {
83
86
  }
84
87
 
85
88
  const main = async () => {
86
- const startTime = performance.now()
87
-
88
89
  const program = new Command()
89
90
 
90
91
  program
@@ -185,12 +186,31 @@ const main = async () => {
185
186
  '仅启动 web 服务,不导出或分析数据(使用 output-wukong/data 中已有的数据)'
186
187
  )
187
188
  .option('--version', 'show version information')
189
+ .option('--profile', '输出性能分析 JSON')
190
+ .option('--verbose', '显示详细性能日志')
191
+ .option('--flame', '显示 flame-like 日志')
192
+ .option('--trace <file>', '生成 Chrome Trace')
193
+ .option('--hot-threshold <n>', 'HOT 比例阈值', parseFloat, 0.8)
194
+ .option('--fail-on-hot', 'HOT 时 CI 失败')
195
+ .option('--diff-base <file>', '基线 profile.json')
196
+ .option('--diff-threshold <n>', '回归阈值', parseFloat, 0.2)
188
197
  .parse()
189
198
 
190
199
  const opts = program.opts()
191
200
 
192
201
  const config = parseOptions(opts)
193
202
 
203
+ profiler = createProfiler({
204
+ enabled: opts.profile,
205
+ verbose: opts.verbose,
206
+ flame: opts.flame,
207
+ traceFile: opts.trace,
208
+ hotThreshold: opts.hotThreshold,
209
+ failOnHot: opts.failOnHot,
210
+ diffBaseFile: opts.diffBase,
211
+ diffThreshold: opts.diffThreshold
212
+ })
213
+
194
214
  // ❗只创建一次缓存实例
195
215
  const getOvertimeStats = createOvertimeStats(config)
196
216
 
@@ -324,6 +344,7 @@ const main = async () => {
324
344
 
325
345
  // --- 分组 ---
326
346
  const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
347
+ profiler.step('load config')
327
348
 
328
349
  // If serve mode is enabled, write data modules and launch the web server
329
350
  if (opts.serve) {
@@ -332,7 +353,12 @@ const main = async () => {
332
353
 
333
354
  // --- Overtime analysis ---
334
355
  if (opts.overtime) {
356
+ await profiler.stepAsync('getOvertimeStats', async () => {
357
+ await getOvertimeStats(records)
358
+ })
335
359
  const stats = getOvertimeStats(records)
360
+
361
+ profiler.step('load getOvertimeStats')
336
362
  // Output to console
337
363
  console.log('\n--- Overtime analysis ---\n')
338
364
  console.log(renderOvertimeText(stats))
@@ -579,7 +605,7 @@ const main = async () => {
579
605
  const jsonText = renderAuthorChangesJson(records)
580
606
  writeJSON(outputFilePath('author-changes.json', outDir), jsonText)
581
607
  logDev(`JSON 已导出: ${filepath}`)
582
- handleSuccess({ startTime, spinner })
608
+ handleSuccess({ spinner })
583
609
  return
584
610
  }
585
611
 
@@ -593,11 +619,11 @@ const main = async () => {
593
619
  renderChangedLinesText(records)
594
620
  )
595
621
 
596
- console.log('\n Commits List:\n', text, '\n')
622
+ // console.log('\n Commits List:\n', text, '\n')
597
623
 
598
624
  logDev(`文本已导出: ${filepath}`)
599
625
 
600
- handleSuccess({ startTime, spinner })
626
+ handleSuccess({ spinner })
601
627
 
602
628
  return
603
629
  }
@@ -621,10 +647,32 @@ const main = async () => {
621
647
  logDev(`Excel 已导出: ${excelPath}`)
622
648
  logDev(`文本已自动导出: ${txtPath}`)
623
649
 
624
- handleSuccess({ startTime, spinner })
650
+ handleSuccess({ spinner })
625
651
  }
626
652
 
627
653
  await autoCheckUpdate()
654
+
655
+ handleSuccess({ spinner })
628
656
  }
629
657
 
630
- main()
658
+ try {
659
+ await main()
660
+ } catch (err) {
661
+ console.error(err)
662
+ process.exitCode = 1
663
+ } finally {
664
+ if (profiler) {
665
+ const result = profiler.end('git-commits')
666
+
667
+ // --profile 时输出 JSON
668
+ if (process.argv.includes('--profile')) {
669
+ const json = {
670
+ command: 'git-commits',
671
+ version: VERSION,
672
+ timestamp: Date.now(),
673
+ profile: result
674
+ }
675
+ console.log(JSON.stringify(json, null, 2))
676
+ }
677
+ }
678
+ }
@@ -4,9 +4,6 @@ import { performance } from 'perf_hooks'
4
4
  import { exitWithTime } from './exitWithTime.mjs'
5
5
  import { costTimer } from './timer.mjs'
6
6
 
7
- export const handleSuccess = ({ message = 'Done', startTime, spinner }) => {
7
+ export const handleSuccess = ({ message = 'Done', spinner }) => {
8
8
  spinner.succeed(message)
9
-
10
- // 如果需要退出,可以调用 exitWithTime
11
- exitWithTime(startTime, 0)
12
9
  }
@@ -0,0 +1,26 @@
1
+ export const diffProfiles = (prev, curr, threshold = 0.2) => {
2
+ const map = new Map()
3
+
4
+ prev.events.forEach(e => {
5
+ map.set(e.name, e.duration)
6
+ })
7
+
8
+ const regressions = []
9
+
10
+ curr.events.forEach(e => {
11
+ const before = map.get(e.name)
12
+ if (!before) return
13
+
14
+ const diff = (e.duration - before) / before
15
+ if (diff >= threshold) {
16
+ regressions.push({
17
+ name: e.name,
18
+ before,
19
+ after: e.duration,
20
+ diff
21
+ })
22
+ }
23
+ })
24
+
25
+ return regressions
26
+ }
@@ -0,0 +1,11 @@
1
+ export const formatTime = (ms) => {
2
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
3
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
4
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
5
+ return `${(ms / 60000).toFixed(2)} min`
6
+ }
7
+
8
+ export const makeBar = (ratio, width = 32) => {
9
+ const len = Math.max(1, Math.round(ratio * width))
10
+ return '█'.repeat(len)
11
+ }
@@ -0,0 +1,144 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs'
3
+ import { formatTime, makeBar } from './format.mjs'
4
+ import { exportChromeTrace } from './trace.mjs'
5
+ import { diffProfiles } from './diff.mjs'
6
+
7
+ export const createProfiler = ({
8
+ enabled = false,
9
+ verbose = false,
10
+ flame = false,
11
+ slowThreshold = 500,
12
+ hotThreshold = 0.8,
13
+ traceFile,
14
+ failOnHot = false,
15
+ diffBaseFile,
16
+ diffThreshold = 0.2
17
+ } = {}) => {
18
+ const start = process.hrtime.bigint()
19
+
20
+
21
+ const stack = []
22
+ const events = []
23
+
24
+ const toMs = (a, b) => Number(b - a) / 1e6
25
+
26
+ /* ================= step ================= */
27
+
28
+ const step = (name, fn) => {
29
+ const parent = stack[stack.length - 1]
30
+ const startTime = process.hrtime.bigint()
31
+
32
+ const node = {
33
+ name,
34
+ start: toMs(start, startTime),
35
+ duration: 0,
36
+ depth: stack.length,
37
+ children: []
38
+ }
39
+
40
+ if (parent) parent.children.push(node)
41
+ stack.push(node)
42
+
43
+ const finish = () => {
44
+ const endTime = process.hrtime.bigint()
45
+ node.duration = toMs(startTime, endTime)
46
+ events.push(node)
47
+ stack.pop()
48
+ }
49
+
50
+ if (typeof fn === 'function') {
51
+ try {
52
+ return fn()
53
+ } finally {
54
+ finish()
55
+ }
56
+ } else {
57
+ finish()
58
+ }
59
+ }
60
+
61
+ /* ================= end ================= */
62
+
63
+ const end = (label = 'Total') => {
64
+ const total = toMs(start, process.hrtime.bigint())
65
+
66
+ let hasHot = false
67
+
68
+ if (enabled || verbose) {
69
+ console.log(
70
+ chalk.cyan('⏱'),
71
+ chalk.bold(label),
72
+ chalk.yellow(formatTime(total))
73
+ )
74
+
75
+ if (flame) {
76
+ for (const e of events) {
77
+ const ratio = e.duration / total
78
+ const hot = ratio >= hotThreshold
79
+ const slow = e.duration >= slowThreshold
80
+
81
+ if (hot) hasHot = true
82
+
83
+ console.log(
84
+ chalk.gray(`${' '.repeat(e.depth) }├─`),
85
+ chalk.white(e.name.padEnd(22)),
86
+ chalk.yellow(formatTime(e.duration)),
87
+ chalk.gray(makeBar(ratio)),
88
+ hot
89
+ ? chalk.red.bold(' 🔥 HOT')
90
+ : slow
91
+ ? chalk.yellow(' ⚠ SLOW')
92
+ : ''
93
+ )
94
+ }
95
+ }
96
+ }
97
+
98
+ const profile = { total, events }
99
+
100
+ /* ---------- Chrome Trace ---------- */
101
+
102
+ if (traceFile) {
103
+ exportChromeTrace(events, traceFile)
104
+ }
105
+
106
+ /* ---------- diff ---------- */
107
+ if (diffBaseFile && fs.existsSync(diffBaseFile)) {
108
+ const base = JSON.parse(fs.readFileSync(diffBaseFile, 'utf8'))
109
+ const regressions = diffProfiles(base, profile, diffThreshold)
110
+
111
+ if (regressions.length) {
112
+ console.log(chalk.red('\n⚠ Performance Regression Detected:\n'))
113
+ regressions.forEach(r => {
114
+ console.log(
115
+ chalk.red(
116
+ ` ${r.name}: ${formatTime(r.before)} → ${formatTime(
117
+ r.after
118
+ )} (+${(r.diff * 100).toFixed(1)}%)`
119
+ )
120
+ )
121
+ })
122
+ process.exitCode = 1
123
+ }
124
+ }
125
+
126
+ /* ---------- save profile ---------- */
127
+ if (enabled) {
128
+ fs.writeFileSync(
129
+ 'profile.json',
130
+ JSON.stringify(profile, null, 2)
131
+ )
132
+ }
133
+
134
+ /* ---------- CI HOT fail ---------- */
135
+ if (failOnHot && hasHot) {
136
+ console.log(chalk.red('\n🔥 HOT step detected — failing CI'))
137
+ process.exitCode = 1
138
+ }
139
+
140
+ return profile
141
+ }
142
+
143
+ return { step, end }
144
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'fs'
2
+
3
+ export const exportChromeTrace = (events, file) => {
4
+ const traceEvents = []
5
+
6
+ for (const e of events) {
7
+ traceEvents.push({
8
+ name: e.name,
9
+ cat: 'cli',
10
+ ph: 'X', // complete event
11
+ ts: e.start * 1000, // μs
12
+ dur: e.duration * 1000, // μs
13
+ pid: 1,
14
+ tid: e.depth || 0
15
+ })
16
+ }
17
+
18
+ const trace = {
19
+ traceEvents,
20
+ displayTimeUnit: 'ms'
21
+ }
22
+
23
+ fs.writeFileSync(file, JSON.stringify(trace, null, 2))
24
+ // TODO: remove debug log before production
25
+ console.log('✅', 'file', file);
26
+ }
@@ -0,0 +1,101 @@
1
+ import chalk from 'chalk'
2
+
3
+ /* ================= utils ================= */
4
+
5
+ const formatTime = (ms) => {
6
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
7
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
8
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
9
+ return `${(ms / 60000).toFixed(2)} min`
10
+ }
11
+
12
+ const makeBar = (ratio, width = 32) => {
13
+ const len = Math.max(1, Math.round(ratio * width))
14
+ return '█'.repeat(len)
15
+ }
16
+
17
+ /* ================= profiler ================= */
18
+
19
+ export const createProfiler = ({
20
+ enabled = false,
21
+ verbose = false,
22
+ flame = false,
23
+ slowThreshold = 500, // ms
24
+ hotThreshold = 0.8 // ratio (0~1)
25
+ } = {}) => {
26
+ const start = process.hrtime.bigint()
27
+ let last = start
28
+ const events = []
29
+
30
+ const toMs = (a, b) => Number(b - a) / 1e6
31
+
32
+ /* -------- step -------- */
33
+ const step = (name, meta = {}) => {
34
+ const now = process.hrtime.bigint()
35
+ const duration = toMs(last, now)
36
+ last = now
37
+
38
+ events.push({
39
+ name,
40
+ duration,
41
+ sinceStart: toMs(start, now),
42
+ meta
43
+ })
44
+
45
+ if (!enabled && !verbose) return
46
+
47
+ const isSlow = duration >= slowThreshold
48
+
49
+ console.log(
50
+ chalk.gray(' ├─'),
51
+ isSlow ? chalk.red.bold(name) : chalk.white(name),
52
+ chalk.yellow(formatTime(duration))
53
+ )
54
+ }
55
+
56
+ /* -------- end -------- */
57
+ const end = (label = 'Total') => {
58
+ const endTime = process.hrtime.bigint()
59
+ const total = toMs(start, endTime)
60
+
61
+ // 计算 HOT
62
+ for (const e of events) {
63
+ e.ratio = total > 0 ? e.duration / total : 0
64
+ e.hot = e.ratio >= hotThreshold
65
+ }
66
+
67
+ if (enabled || verbose) {
68
+ console.log(
69
+ chalk.cyan('⏱'),
70
+ chalk.bold(label),
71
+ chalk.yellow(formatTime(total))
72
+ )
73
+
74
+ if (flame) {
75
+ for (const e of events) {
76
+ const bar = makeBar(e.ratio)
77
+ const hotMark = e.hot ? chalk.red.bold(' 🔥 HOT') : ''
78
+ const slowMark =
79
+ !e.hot && e.duration >= slowThreshold
80
+ ? chalk.yellow(' ⚠ SLOW')
81
+ : ''
82
+
83
+ console.log(
84
+ chalk.gray(' ├─'),
85
+ chalk.white(e.name.padEnd(24)),
86
+ chalk.yellow(formatTime(e.duration)),
87
+ chalk.gray(bar),
88
+ hotMark || slowMark
89
+ )
90
+ }
91
+ }
92
+ }
93
+
94
+ return {
95
+ total,
96
+ events
97
+ }
98
+ }
99
+
100
+ return { step, end }
101
+ }
@@ -0,0 +1,37 @@
1
+ // utils/scopeTimer.js
2
+ import chalk from 'chalk'
3
+
4
+ const formatTime = (ms) => {
5
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
6
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
7
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
8
+ return `${(ms / 60000).toFixed(2)} min`
9
+ }
10
+
11
+ export const createScopeTimer = (scope) => {
12
+ const start = process.hrtime.bigint()
13
+ let last = start
14
+
15
+ const step = (label) => {
16
+ const now = process.hrtime.bigint()
17
+ const ms = Number(now - last) / 1e6
18
+ last = now
19
+
20
+ console.log(
21
+ chalk.gray(' ├─'),
22
+ chalk.white(label),
23
+ chalk.yellow(formatTime(ms))
24
+ )
25
+ }
26
+
27
+ const end = () => {
28
+ const total = Number(process.hrtime.bigint() - start) / 1e6
29
+ console.log(
30
+ chalk.cyan('⏱'),
31
+ chalk.bold(scope),
32
+ chalk.yellow(formatTime(total))
33
+ )
34
+ }
35
+
36
+ return { step, end }
37
+ }
@@ -0,0 +1,33 @@
1
+ // utils/timer.js
2
+ import chalk from 'chalk'
3
+
4
+ const ENABLED = process.env.CLI_TIMER !== 'false'
5
+
6
+ const formatTime = (ms) => {
7
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
8
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
9
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
10
+ return `${(ms / 60000).toFixed(2)} min`
11
+ }
12
+
13
+ export const createTimer = (label, { silent = false } = {}) => {
14
+ const start = process.hrtime.bigint()
15
+
16
+ const stop = (suffix) => {
17
+ if (!ENABLED || silent) return
18
+
19
+ const end = process.hrtime.bigint()
20
+ const ms = Number(end - start) / 1e6
21
+
22
+ console.log(
23
+ chalk.cyan('⏱'),
24
+ chalk.white(label),
25
+ suffix ? chalk.gray(`(${suffix})`) : '',
26
+ chalk.bold.yellow(formatTime(ms))
27
+ )
28
+
29
+ return ms
30
+ }
31
+
32
+ return { stop }
33
+ }
@@ -0,0 +1,11 @@
1
+ // utils/withTimer.js
2
+ import { createTimer } from './timer.mjs'
3
+
4
+ export const withTimer = async (label, fn, opts) => {
5
+ const timer = createTimer(label, opts)
6
+ try {
7
+ return await fn()
8
+ } finally {
9
+ timer.stop()
10
+ }
11
+ }