wukong-gitlog-cli 1.0.38 → 1.0.40
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 +1 -0
- package/.prettierrc +2 -1
- package/CHANGELOG.md +103 -0
- package/README.md +93 -173
- package/README.zh-CN.md +85 -137
- package/doc//347/233/256/345/275/225/347/273/223/346/236/204.md +2871 -0
- package/package.json +33 -29
- package/rc/.wukonggitlogrc +53 -0
- package/scripts/compareHourlyCounts.mjs +42 -0
- package/scripts/compareLatest.mjs +106 -0
- package/src/app/analyzeAction.mjs +120 -0
- package/src/app/exportAction.mjs +215 -0
- package/src/app/exportActionProgress.mjs +37 -0
- package/src/app/helpers.mjs +292 -0
- package/src/app/initAction.mjs +110 -0
- package/src/app/initActionWithTemp.mjs +192 -0
- package/src/app/journalAction.mjs +117 -0
- package/src/app/overtimeAction.mjs +100 -0
- package/src/app/runProfileEnd.mjs +0 -0
- package/src/app/serveAction.mjs +73 -0
- package/src/app/versionAction.mjs +7 -0
- package/src/cli/defineOptions.mjs +209 -0
- package/src/cli/index.mjs +0 -0
- package/src/cli/parseOptions.mjs +126 -8
- package/src/constants/index.mjs +16 -2
- package/src/domain/author/analyze.mjs +6 -0
- package/src/domain/author/map.mjs +0 -0
- package/src/domain/export/exportAuthor.mjs +28 -0
- package/src/domain/export/exportAuthorChanges.mjs +27 -0
- package/src/domain/export/exportAuthorChangesJson.mjs +31 -0
- package/src/domain/export/exportByMonth.mjs +157 -0
- package/src/domain/export/exportByWeek.mjs +121 -0
- package/src/domain/export/exportCommits.mjs +26 -0
- package/src/domain/export/exportCommitsExcel.mjs +45 -0
- package/src/domain/export/exportCommitsJson.mjs +31 -0
- package/src/domain/export/index.mjs +91 -0
- package/src/domain/git/ensureGitAvailable.mjs +66 -0
- package/src/domain/git/ensureGitRepo.mjs +41 -0
- package/src/domain/git/getGitFeatures.mjs +59 -0
- package/src/domain/git/getGitLogs.mjs +326 -0
- package/src/domain/git/getGitUser.mjs +44 -0
- package/src/domain/git/getRepoRoot.mjs +32 -0
- package/src/domain/git/gitCapability.mjs +119 -0
- package/src/domain/git/index.mjs +96 -0
- package/src/domain/git/resolveGerrit.mjs +102 -0
- package/src/domain/overtime/analyze.mjs +48 -0
- package/src/domain/overtime/index.mjs +3 -0
- package/src/domain/overtime/perPeriod.mjs +15 -0
- package/src/domain/overtime/render.mjs +15 -0
- package/src/i18n/index.mjs +38 -0
- package/src/i18n/resources.mjs +252 -0
- package/src/index.mjs +132 -649
- package/src/infra/cache.mjs +0 -0
- package/src/infra/configStore.mjs +128 -0
- package/src/infra/fs.mjs +0 -0
- package/src/infra/path.mjs +0 -0
- package/src/output/csv/overtime.mjs +12 -0
- package/src/output/csv.mjs +0 -0
- package/src/output/data/readData.mjs +54 -0
- package/src/output/data/writeData.mjs +145 -0
- package/src/output/excel/commits.mjs +9 -0
- package/src/output/excel/outputExcelDayReport.mjs +92 -0
- package/src/output/excel/perPeriod.mjs +24 -0
- package/src/{excel.mjs → output/excel.mjs} +3 -2
- package/src/output/index.mjs +79 -0
- package/src/output/json/overtime.mjs +9 -0
- package/src/output/tab/overtime.mjs +12 -0
- package/src/output/tab.mjs +0 -0
- package/src/output/text/commits.mjs +9 -0
- package/src/output/text/index.mjs +3 -0
- package/src/output/text/outputTxtDayReport.mjs +74 -0
- package/src/output/text/overtime.mjs +18 -0
- package/src/output/utils/getEsmJs.mjs +10 -0
- package/src/output/utils/index.mjs +14 -0
- package/src/output/utils/outputPath.mjs +19 -0
- package/src/output/utils/writeFile.mjs +10 -0
- package/src/serve/index.mjs +0 -0
- package/src/{server.mjs → serve/startServer.mjs} +21 -3
- package/src/serve/writeData.mjs +0 -0
- package/src/utils/authorNormalizer.mjs +28 -2
- package/src/utils/buildAuthorChangeStats.mjs +44 -0
- package/src/utils/deepMerge.mjs +13 -0
- package/src/utils/getPackage.mjs +11 -0
- package/src/utils/getProfileDirFile.mjs +12 -0
- package/src/utils/{file.mjs → groupRecords.mjs} +8 -9
- package/src/utils/index.mjs +5 -2
- package/src/utils/logger.mjs +28 -17
- package/src/utils/profiler.mjs +0 -101
- package/src/utils/resolve.mjs +11 -0
- package/src/utils/showVersionInfo.mjs +6 -2
- package/src/utils/time.mjs +0 -0
- package/src/utils/wait.mjs +2 -0
- package/web/app.js +3233 -260
- package/web/index.html +175 -22
- package/web/revoke/alpha1/app.js +4324 -0
- package/web/revoke/alpha1/index.html +266 -0
- package/web/revoke/app.before.js +3139 -0
- package/web/revoke/index-before.html +181 -0
- package/web/static/style.css +155 -9
- package/src/git.mjs +0 -256
- package/src/handlers/handleServe.mjs +0 -203
- package/src/lib/configStore.mjs +0 -11
- package/src/lib/memoize.mjs +0 -14
- package/src/utils/analyzeOvertimeCached.mjs +0 -7
- package/src/utils/checkUpdate.mjs +0 -130
- package/src/utils/exitWithTime.mjs +0 -17
- package/src/utils/handleSuccess.mjs +0 -9
- package/src/utils/logDev.mjs +0 -19
- package/src/utils/output.mjs +0 -26
- package/src/utils/profiler/diff.mjs +0 -26
- package/src/utils/profiler/format.mjs +0 -11
- package/src/utils/profiler/index.mjs +0 -144
- package/src/utils/profiler/trace.mjs +0 -26
- package/src/utils/time/scopeTimer.mjs +0 -37
- package/src/utils/time/timer.mjs +0 -33
- package/src/utils/time/withTimer.mjs +0 -11
- package/src/utils/timer.mjs +0 -35
- /package/src/{overtime → domain/overtime}/createOvertimeStats.mjs +0 -0
- /package/src/{overtime → domain/overtime}/overtime.mjs +0 -0
- /package/src/{json.mjs → output/json.mjs} +0 -0
- /package/src/{renderAuthorMapText.mjs → output/renderAuthorMapText.mjs} +0 -0
- /package/src/{stats-text.mjs → output/stats-text.mjs} +0 -0
- /package/src/{stats.mjs → output/stats.mjs} +0 -0
- /package/src/{text.mjs → output/text.mjs} +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import { DAY_REPORT_TXT } from '#src/constants/index.mjs'
|
|
6
|
+
import { outFile } from '#src/output/utils/outputPath.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @function outputTxtDayReport
|
|
10
|
+
* @description
|
|
11
|
+
* 按 author 输出 TXT 日报(每人一个文件)
|
|
12
|
+
*/
|
|
13
|
+
export const outputTxtDayReport = async ({
|
|
14
|
+
dayReports = [],
|
|
15
|
+
conf = {}
|
|
16
|
+
} = {}) => {
|
|
17
|
+
if (!Array.isArray(dayReports) || dayReports.length === 0) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = { dir: conf.dir || path.resolve('output-wukong') }
|
|
22
|
+
const baseDir = `${config.dir}/${DAY_REPORT_TXT}`
|
|
23
|
+
|
|
24
|
+
// 生成时间(来自上层 outputData)
|
|
25
|
+
const generateTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
26
|
+
|
|
27
|
+
// 按 author 分组
|
|
28
|
+
const authorMap = {}
|
|
29
|
+
dayReports.forEach((item) => {
|
|
30
|
+
if (!authorMap[item.author]) {
|
|
31
|
+
authorMap[item.author] = []
|
|
32
|
+
}
|
|
33
|
+
authorMap[item.author].push(item)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// 每个人生成一个 txt
|
|
37
|
+
for (const [author, records] of Object.entries(authorMap)) {
|
|
38
|
+
// 日期升序
|
|
39
|
+
records.sort(
|
|
40
|
+
(a, b) =>
|
|
41
|
+
dayjs(a.day).valueOf() - dayjs(b.day).valueOf()
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const lines = []
|
|
45
|
+
|
|
46
|
+
// ✅ 文件头:生成时间
|
|
47
|
+
lines.push(`生成时间:${generateTime}`)
|
|
48
|
+
lines.push('')
|
|
49
|
+
|
|
50
|
+
// 姓名
|
|
51
|
+
lines.push(`姓名:${author}`)
|
|
52
|
+
lines.push('')
|
|
53
|
+
|
|
54
|
+
// 每一天
|
|
55
|
+
records.forEach((item) => {
|
|
56
|
+
lines.push(item.day)
|
|
57
|
+
lines.push('--------------------------------')
|
|
58
|
+
lines.push('工作时长:8 小时')
|
|
59
|
+
lines.push('工作内容:')
|
|
60
|
+
lines.push(item.msg)
|
|
61
|
+
lines.push('') // 空行分隔
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const content = lines.join('\n')
|
|
65
|
+
|
|
66
|
+
const txtFile = `${author}.txt`
|
|
67
|
+
const filePath = outFile(baseDir, txtFile)
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line no-await-in-loop
|
|
70
|
+
fs.writeFileSync(filePath, content, 'utf-8')
|
|
71
|
+
|
|
72
|
+
console.log(`✅ 已生成 TXT:${filePath}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { writeText } from '../utils/writeFile.mjs'
|
|
2
|
+
import { outFile } from '../utils/outputPath.mjs'
|
|
3
|
+
import {
|
|
4
|
+
renderOvertimeText,
|
|
5
|
+
renderOvertimeTab
|
|
6
|
+
} from '../../domain/overtime/render.mjs'
|
|
7
|
+
|
|
8
|
+
export function outputOvertimeText(stats, config) {
|
|
9
|
+
writeText(
|
|
10
|
+
outFile(config.dir, `overtime_${config.base}.txt`),
|
|
11
|
+
renderOvertimeText(stats)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
writeText(
|
|
15
|
+
outFile(config.dir, `overtime_${config.base}.tab.txt`),
|
|
16
|
+
renderOvertimeTab(stats)
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { outFile } from './outputPath.mjs'
|
|
2
|
+
import { writeJson, writeText } from './writeFile.mjs'
|
|
3
|
+
|
|
4
|
+
/* ---------------- helpers ---------------- */
|
|
5
|
+
|
|
6
|
+
export function writeJsonFile(dir, name, data) {
|
|
7
|
+
writeJson(outFile(dir, name), data)
|
|
8
|
+
return name
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function writeTxtFile(dir, name, data) {
|
|
12
|
+
writeText(outFile(dir, name), data)
|
|
13
|
+
return name
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
|
|
4
|
+
export function resolveOutDir(baseDir = 'output-wukong') {
|
|
5
|
+
const dir = path.isAbsolute(baseDir)
|
|
6
|
+
? baseDir
|
|
7
|
+
: path.resolve(process.cwd(), baseDir)
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(dir)) {
|
|
10
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
11
|
+
}
|
|
12
|
+
return dir
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function outFile(dir, filename) {
|
|
16
|
+
const full = path.join(dir, filename)
|
|
17
|
+
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
18
|
+
return full
|
|
19
|
+
}
|
|
File without changes
|
|
@@ -40,16 +40,31 @@ function openBrowser(url) {
|
|
|
40
40
|
})
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// 辅助函数:向上寻找包含 package.json 的根目录
|
|
44
|
+
function findPkgRoot(currentDir) {
|
|
45
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
46
|
+
return currentDir
|
|
47
|
+
}
|
|
48
|
+
const parentDir = path.resolve(currentDir, '..')
|
|
49
|
+
if (parentDir === currentDir) return currentDir // 已到系统根目录
|
|
50
|
+
return findPkgRoot(parentDir)
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
// eslint-disable-next-line default-param-last
|
|
44
54
|
export function startServer(port = 3000, outputDir) {
|
|
45
55
|
// 解析包根目录,确保 web 资源在全局安装后也能找到
|
|
46
|
-
const
|
|
47
|
-
const
|
|
56
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
57
|
+
const __dirname = path.dirname(__filename)
|
|
58
|
+
|
|
59
|
+
// 动态寻找根目录
|
|
60
|
+
const pkgRoot = findPkgRoot(__dirname)
|
|
61
|
+
|
|
48
62
|
const webRoot = path.resolve(pkgRoot, 'web')
|
|
49
63
|
const dataRoot = outputDir
|
|
50
64
|
? path.resolve(outputDir)
|
|
51
65
|
: path.resolve(process.cwd(), 'output-wukong')
|
|
52
66
|
|
|
67
|
+
|
|
53
68
|
// warn if web directory or data directory doesn't exist
|
|
54
69
|
if (!fs.existsSync(webRoot)) {
|
|
55
70
|
console.warn(
|
|
@@ -116,7 +131,10 @@ export function startServer(port = 3000, outputDir) {
|
|
|
116
131
|
return new Promise((resolve, reject) => {
|
|
117
132
|
server.on('error', (err) => reject(err))
|
|
118
133
|
server.listen(port, () => {
|
|
119
|
-
|
|
134
|
+
// 监听到的真实端口(当 port 为 0 或系统分配端口时很重要)
|
|
135
|
+
const bound = server.address()
|
|
136
|
+
const actualPort = bound && bound.port ? bound.port : port
|
|
137
|
+
const url = `http://localhost:${actualPort}`
|
|
120
138
|
console.log(chalk.green(`Server started at ${url}`))
|
|
121
139
|
console.log(chalk.green(`Serving web/ and output-wukong/data/`))
|
|
122
140
|
|
|
File without changes
|
|
@@ -4,10 +4,20 @@
|
|
|
4
4
|
* - 同邮箱多个英文名保持第一个
|
|
5
5
|
* - 保留原始 author 以便 debug
|
|
6
6
|
*/
|
|
7
|
-
export function createAuthorNormalizer() {
|
|
7
|
+
export function createAuthorNormalizer(aliases = {}) {
|
|
8
8
|
const map = {} // email -> canonical name
|
|
9
9
|
const originalMap = {} // email -> Set of original author names
|
|
10
10
|
|
|
11
|
+
// 预处理别名配置:按 email/name 划分
|
|
12
|
+
const aliasByEmail = {}
|
|
13
|
+
const aliasByName = {}
|
|
14
|
+
for (const [k, v] of Object.entries(aliases || {})) {
|
|
15
|
+
const key = (k || '').trim()
|
|
16
|
+
if (!key) continue
|
|
17
|
+
if (key.includes('@')) aliasByEmail[key] = v
|
|
18
|
+
else aliasByName[key] = v
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
function isChinese(str) {
|
|
12
22
|
return /[\u4e00-\u9fa5]/.test(str)
|
|
13
23
|
}
|
|
@@ -25,12 +35,28 @@ export function createAuthorNormalizer() {
|
|
|
25
35
|
const n = cleanName(name)
|
|
26
36
|
const e = cleanEmail(email)
|
|
27
37
|
|
|
28
|
-
if (!e)
|
|
38
|
+
if (!e) {
|
|
39
|
+
// 如果没有邮箱,先尝试按名字别名匹配
|
|
40
|
+
if (n && aliasByName[n]) return aliasByName[n]
|
|
41
|
+
return n || 'Unknown'
|
|
42
|
+
}
|
|
29
43
|
|
|
30
44
|
// 记录原始 author
|
|
31
45
|
if (!originalMap[e]) originalMap[e] = new Set()
|
|
32
46
|
if (n) originalMap[e].add(n)
|
|
33
47
|
|
|
48
|
+
// 先检查 email 别名配置
|
|
49
|
+
if (aliasByEmail[e]) {
|
|
50
|
+
map[e] = aliasByEmail[e]
|
|
51
|
+
return map[e]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 再检查名字别名配置
|
|
55
|
+
if (n && aliasByName[n]) {
|
|
56
|
+
map[e] = aliasByName[n]
|
|
57
|
+
return map[e]
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
const prev = map[e]
|
|
35
61
|
|
|
36
62
|
// 中文名优先覆盖
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import isoWeek from 'dayjs/plugin/isoWeek.js'
|
|
3
|
+
|
|
4
|
+
dayjs.extend(isoWeek)
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 自动统计每个作者的日/周/月 changed
|
|
8
|
+
*
|
|
9
|
+
* records: [{
|
|
10
|
+
* author: 'Tom',
|
|
11
|
+
* date: '2025-01-01T12:00:00Z',
|
|
12
|
+
* changed: 123
|
|
13
|
+
* }]
|
|
14
|
+
*/
|
|
15
|
+
export function buildAuthorChangeStats(records) {
|
|
16
|
+
const result = {
|
|
17
|
+
daily: {}, // { author: { 'YYYY-MM-DD': x } }
|
|
18
|
+
weekly: {}, // { author: { 'YYYY-WW': x } }
|
|
19
|
+
monthly: {} // { author: { 'YYYY-MM': x } }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const r of records) {
|
|
23
|
+
const author = r.author || 'Unknown'
|
|
24
|
+
const changed = Number(r.changed || 0)
|
|
25
|
+
|
|
26
|
+
const d = dayjs(r.date)
|
|
27
|
+
|
|
28
|
+
const dayKey = d.format('YYYY-MM-DD')
|
|
29
|
+
const weekKey = `${d.format('GGGG')}-W${d.isoWeek().toString().padStart(2, '0')}`
|
|
30
|
+
const monthKey = d.format('YYYY-MM')
|
|
31
|
+
|
|
32
|
+
if (!result.daily[author]) result.daily[author] = {}
|
|
33
|
+
if (!result.weekly[author]) result.weekly[author] = {}
|
|
34
|
+
if (!result.monthly[author]) result.monthly[author] = {}
|
|
35
|
+
|
|
36
|
+
result.daily[author][dayKey] = (result.daily[author][dayKey] || 0) + changed
|
|
37
|
+
result.weekly[author][weekKey] =
|
|
38
|
+
(result.weekly[author][weekKey] || 0) + changed
|
|
39
|
+
result.monthly[author][monthKey] =
|
|
40
|
+
(result.monthly[author][monthKey] || 0) + changed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function deepMerge(target, source) {
|
|
2
|
+
for (const key in source) {
|
|
3
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
4
|
+
// eslint-disable-next-line no-param-reassign
|
|
5
|
+
if (!target[key]) target[key] = {};
|
|
6
|
+
deepMerge(target[key], source[key]);
|
|
7
|
+
} else if (source[key] !== undefined) {
|
|
8
|
+
// eslint-disable-next-line no-param-reassign
|
|
9
|
+
target[key] = source[key];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return target;
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
export const getPackage = () => {
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const pkg = JSON.parse(
|
|
8
|
+
fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf8')
|
|
9
|
+
)
|
|
10
|
+
return pkg
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
import { EXPORT_DIR_PROFILE } from '#src/constants/index.mjs'
|
|
4
|
+
import { outFile } from '#src/output/utils/outputPath.mjs'
|
|
5
|
+
|
|
6
|
+
export const getProfileDirFile = (fileName, opts) => {
|
|
7
|
+
const config = { dir: opts.output.dir || path.resolve('output-wukong') }
|
|
8
|
+
const baseDir = `${config.dir}/${EXPORT_DIR_PROFILE}`
|
|
9
|
+
const filePath = outFile(baseDir, fileName)
|
|
10
|
+
|
|
11
|
+
return filePath
|
|
12
|
+
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file: groupRecords.mjs
|
|
3
|
+
* @description: 按指定周期对提交记录进行分组
|
|
4
|
+
* @author: King Monkey
|
|
5
|
+
* @created: 2026-01-04 00:10
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import dayjs from 'dayjs'
|
|
2
9
|
import isoWeek from 'dayjs/plugin/isoWeek.js'
|
|
3
|
-
|
|
10
|
+
|
|
4
11
|
|
|
5
12
|
// add ISO week plugin to dayjs once when module loaded
|
|
6
13
|
dayjs.extend(isoWeek)
|
|
7
14
|
|
|
8
|
-
export function writeJSON(file, data) {
|
|
9
|
-
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8')
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function writeTextFile(file, text) {
|
|
13
|
-
fs.writeFileSync(file, text, 'utf8')
|
|
14
|
-
}
|
|
15
|
-
|
|
16
15
|
export function groupRecords(records, mode) {
|
|
17
16
|
const group = {}
|
|
18
17
|
|
package/src/utils/index.mjs
CHANGED
package/src/utils/logger.mjs
CHANGED
|
@@ -34,15 +34,6 @@ if (!process.stdout.isTTY || chalk.level === 0) {
|
|
|
34
34
|
let cachedDay = ''
|
|
35
35
|
let cachedPath = ''
|
|
36
36
|
|
|
37
|
-
// // 彩色前缀
|
|
38
|
-
// const prefix = {
|
|
39
|
-
// info: chalk.cyan('ℹ'),
|
|
40
|
-
// success: chalk.green('✔'),
|
|
41
|
-
// error: chalk.red('✖'),
|
|
42
|
-
// warn: chalk.yellow('⚠'),
|
|
43
|
-
// debug: chalk.gray('➤')
|
|
44
|
-
// }
|
|
45
|
-
|
|
46
37
|
// 🔧 1. 修改 prefix:只保留 emoji,不带颜色
|
|
47
38
|
const prefix = {
|
|
48
39
|
info: e('➤', '[i]'),
|
|
@@ -89,6 +80,20 @@ const getLogFilePath = () => {
|
|
|
89
80
|
return cachedPath
|
|
90
81
|
}
|
|
91
82
|
|
|
83
|
+
// 格式化参数为字符串
|
|
84
|
+
const formatArgs = (args) => {
|
|
85
|
+
return args.map(arg => {
|
|
86
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.stringify(arg, null, 2)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return String(arg)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return String(arg)
|
|
94
|
+
}).join(' ')
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
// 写入日志(同步 + 追加)
|
|
93
98
|
const writeToFile = (level, msg, newline) => {
|
|
94
99
|
const logPath = getLogFilePath()
|
|
@@ -105,6 +110,8 @@ const writeToFile = (level, msg, newline) => {
|
|
|
105
110
|
function createLogger(level, colorFn, outFn = console.log) {
|
|
106
111
|
return (...args) => {
|
|
107
112
|
let options = {}
|
|
113
|
+
|
|
114
|
+
// 检查最后一个参数是否是选项对象
|
|
108
115
|
if (
|
|
109
116
|
args.length &&
|
|
110
117
|
typeof args[args.length - 1] === 'object' &&
|
|
@@ -114,22 +121,26 @@ function createLogger(level, colorFn, outFn = console.log) {
|
|
|
114
121
|
options = args.pop()
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
// 处理剩余的参数
|
|
125
|
+
const formattedArgs = formatArgs(args)
|
|
126
|
+
const displayMsg = args.length === 1 && typeof args[0] === 'string'
|
|
127
|
+
? args[0]
|
|
128
|
+
: formattedArgs
|
|
122
129
|
|
|
123
130
|
// 🔧 2. 修改拼接方式,colorFn 统一处理 prefix + msg
|
|
124
|
-
const
|
|
131
|
+
const timestamp = shortTimestamp()
|
|
132
|
+
const prefixStr = prefix[level]
|
|
125
133
|
|
|
126
134
|
if (options.newline) {
|
|
127
135
|
outFn('')
|
|
128
136
|
}
|
|
129
|
-
outFn(line)
|
|
130
137
|
|
|
138
|
+
// 输出到控制台,保留参数的原格式
|
|
139
|
+
outFn(timestamp, colorFn(prefixStr), ...args)
|
|
140
|
+
|
|
141
|
+
// 写入文件时使用格式化的字符串
|
|
131
142
|
if (options.write) {
|
|
132
|
-
writeToFile(level,
|
|
143
|
+
writeToFile(level, displayMsg, options.newline)
|
|
133
144
|
}
|
|
134
145
|
}
|
|
135
146
|
}
|
package/src/utils/profiler.mjs
CHANGED
|
@@ -1,101 +0,0 @@
|
|
|
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,11 @@
|
|
|
1
|
+
export function resolveBool(cli, config, def = false) {
|
|
2
|
+
if (cli !== undefined) return cli
|
|
3
|
+
if (config !== undefined) return config
|
|
4
|
+
return def
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveValue(cli, config, def) {
|
|
8
|
+
if (cli !== undefined) return cli
|
|
9
|
+
if (config !== undefined) return config
|
|
10
|
+
return def
|
|
11
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import os from 'os'
|
|
2
2
|
import process from 'process'
|
|
3
|
+
|
|
3
4
|
import { colors } from './colors.mjs'
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
export function showVersionInfo(pkg) {
|
|
9
|
+
const { name, version } = pkg
|
|
10
|
+
|
|
11
|
+
console.log(colors.title(`\n${name} v${version}\n`))
|
|
8
12
|
console.log(`${colors.bold('Node.js')}: ${process.version}`)
|
|
9
13
|
console.log(`${colors.bold('Platform')}: ${os.platform()} ${os.arch()}`)
|
|
10
14
|
}
|
|
File without changes
|