openspec-stat 1.0.0 → 1.1.0
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/README.md +2 -0
- package/dist/cjs/cli.js +20 -37
- package/dist/cjs/formatters.js +4 -12
- package/dist/cjs/git-analyzer.js +10 -23
- package/dist/cjs/i18n/index.js +1 -1
- package/dist/cjs/i18n/locales/en.json +8 -8
- package/dist/cjs/i18n/locales/zh-CN.json +8 -8
- package/dist/cjs/stats-aggregator.js +10 -5
- package/dist/esm/cli.js +1 -1
- package/dist/esm/git-analyzer.js +5 -3
- package/dist/esm/i18n/index.js +1 -1
- package/dist/esm/i18n/locales/en.json +8 -8
- package/dist/esm/i18n/locales/zh-CN.json +8 -8
- package/dist/esm/stats-aggregator.js +45 -19
- package/package.json +35 -3
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://npmjs.com/package/openspec-stat)
|
|
4
4
|
[](https://npmjs.com/package/openspec-stat)
|
|
5
5
|
|
|
6
|
+
English | [简体中文](./README.zh-CN.md)
|
|
7
|
+
|
|
6
8
|
A CLI tool for tracking team members' OpenSpec proposals and code changes in Git repositories.
|
|
7
9
|
|
|
8
10
|
## Features
|
package/dist/cjs/cli.js
CHANGED
|
@@ -41,19 +41,10 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
|
|
|
41
41
|
let since;
|
|
42
42
|
let until;
|
|
43
43
|
if (options.since || options.until) {
|
|
44
|
-
since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(
|
|
45
|
-
|
|
46
|
-
config.defaultUntilHours
|
|
47
|
-
).since;
|
|
48
|
-
until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(
|
|
49
|
-
config.defaultSinceHours,
|
|
50
|
-
config.defaultUntilHours
|
|
51
|
-
).until;
|
|
44
|
+
since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).since;
|
|
45
|
+
until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).until;
|
|
52
46
|
} else {
|
|
53
|
-
const defaultRange = (0, import_time_utils.getDefaultTimeRange)(
|
|
54
|
-
config.defaultSinceHours,
|
|
55
|
-
config.defaultUntilHours
|
|
56
|
-
);
|
|
47
|
+
const defaultRange = (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours);
|
|
57
48
|
since = defaultRange.since;
|
|
58
49
|
until = defaultRange.until;
|
|
59
50
|
}
|
|
@@ -73,14 +64,16 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
|
|
|
73
64
|
})
|
|
74
65
|
)
|
|
75
66
|
);
|
|
76
|
-
console.log(
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
console.log(
|
|
68
|
+
import_chalk.default.blue(
|
|
69
|
+
(0, import_i18n.t)("info.branches", {
|
|
70
|
+
branches: branches.join(", ") || (0, import_i18n.t)("info.allBranches")
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
);
|
|
79
74
|
const analyzer = new import_git_analyzer.GitAnalyzer(options.repo, config);
|
|
80
75
|
console.log(import_chalk.default.blue((0, import_i18n.t)("loading.activeUsers")));
|
|
81
|
-
const activeAuthors = await analyzer.getActiveAuthors(
|
|
82
|
-
config.activeUserWeeks || 2
|
|
83
|
-
);
|
|
76
|
+
const activeAuthors = await analyzer.getActiveAuthors(config.activeUserWeeks || 2);
|
|
84
77
|
if (options.verbose) {
|
|
85
78
|
console.log(
|
|
86
79
|
import_chalk.default.gray(
|
|
@@ -103,10 +96,12 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
|
|
|
103
96
|
const commit = commits[i];
|
|
104
97
|
if (options.verbose && i % 10 === 0) {
|
|
105
98
|
console.log(
|
|
106
|
-
import_chalk.default.gray(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
import_chalk.default.gray(
|
|
100
|
+
(0, import_i18n.t)("info.analysisProgress", {
|
|
101
|
+
current: String(i + 1),
|
|
102
|
+
total: String(commits.length)
|
|
103
|
+
})
|
|
104
|
+
)
|
|
110
105
|
);
|
|
111
106
|
}
|
|
112
107
|
const analysis = await analyzer.analyzeCommit(commit);
|
|
@@ -115,24 +110,12 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
|
|
|
115
110
|
}
|
|
116
111
|
}
|
|
117
112
|
if (analyses.length === 0) {
|
|
118
|
-
console.log(
|
|
119
|
-
import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits"))
|
|
120
|
-
);
|
|
113
|
+
console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits")));
|
|
121
114
|
return;
|
|
122
115
|
}
|
|
123
|
-
console.log(
|
|
124
|
-
import_chalk.default.blue(
|
|
125
|
-
(0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })
|
|
126
|
-
)
|
|
127
|
-
);
|
|
116
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })));
|
|
128
117
|
const aggregator = new import_stats_aggregator.StatsAggregator(config, activeAuthors);
|
|
129
|
-
const result = aggregator.aggregate(
|
|
130
|
-
analyses,
|
|
131
|
-
since,
|
|
132
|
-
until,
|
|
133
|
-
branches,
|
|
134
|
-
options.author
|
|
135
|
-
);
|
|
118
|
+
const result = aggregator.aggregate(analyses, since, until, branches, options.author);
|
|
136
119
|
const formatter = new import_formatters.OutputFormatter();
|
|
137
120
|
if (options.json) {
|
|
138
121
|
console.log(formatter.formatJSON(result));
|
package/dist/cjs/formatters.js
CHANGED
|
@@ -47,9 +47,7 @@ var OutputFormatter = class {
|
|
|
47
47
|
);
|
|
48
48
|
output += import_chalk.default.gray((0, import_i18n.t)("output.branches", { branches: result.branches.join(", ") }));
|
|
49
49
|
output += import_chalk.default.gray((0, import_i18n.t)("output.totalCommits", { count: String(result.totalCommits) }));
|
|
50
|
-
const sortedAuthors = Array.from(result.authors.values()).sort(
|
|
51
|
-
(a, b) => b.commits - a.commits
|
|
52
|
-
);
|
|
50
|
+
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
53
51
|
for (const stats of sortedAuthors) {
|
|
54
52
|
output += import_chalk.default.bold.cyan(`
|
|
55
53
|
${stats.author}
|
|
@@ -70,9 +68,7 @@ ${stats.author}
|
|
|
70
68
|
border: []
|
|
71
69
|
}
|
|
72
70
|
});
|
|
73
|
-
const sortedBranches = Array.from(stats.branchStats.values()).sort(
|
|
74
|
-
(a, b) => b.commits - a.commits
|
|
75
|
-
);
|
|
71
|
+
const sortedBranches = Array.from(stats.branchStats.values()).sort((a, b) => b.commits - a.commits);
|
|
76
72
|
for (const branchStat of sortedBranches) {
|
|
77
73
|
branchTable.push([
|
|
78
74
|
branchStat.branch,
|
|
@@ -158,9 +154,7 @@ ${stats.author}
|
|
|
158
154
|
rows.push(
|
|
159
155
|
`${(0, import_i18n.t)("table.author")},${(0, import_i18n.t)("table.period")},${(0, import_i18n.t)("table.commits")},${(0, import_i18n.t)("table.proposalsCount")},${(0, import_i18n.t)("table.proposalsList")},${(0, import_i18n.t)("table.codeFiles")},${(0, import_i18n.t)("table.additions")},${(0, import_i18n.t)("table.deletions")},${(0, import_i18n.t)("table.netChanges")},${(0, import_i18n.t)("table.lastCommitDate")}`
|
|
160
156
|
);
|
|
161
|
-
const sortedAuthors = Array.from(result.authors.values()).sort(
|
|
162
|
-
(a, b) => b.commits - a.commits
|
|
163
|
-
);
|
|
157
|
+
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
164
158
|
for (const stats of sortedAuthors) {
|
|
165
159
|
const proposals = Array.from(stats.openspecProposals).join(";");
|
|
166
160
|
rows.push(
|
|
@@ -193,9 +187,7 @@ ${stats.author}
|
|
|
193
187
|
md += `| ${(0, import_i18n.t)("table.author")} | ${(0, import_i18n.t)("table.period")} | ${(0, import_i18n.t)("table.commits")} | ${(0, import_i18n.t)("table.proposals")} | ${(0, import_i18n.t)("table.codeFiles")} | ${(0, import_i18n.t)("table.additions")} | ${(0, import_i18n.t)("table.deletions")} | ${(0, import_i18n.t)("table.netChanges")} |
|
|
194
188
|
`;
|
|
195
189
|
md += "|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n";
|
|
196
|
-
const sortedAuthors = Array.from(result.authors.values()).sort(
|
|
197
|
-
(a, b) => b.commits - a.commits
|
|
198
|
-
);
|
|
190
|
+
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
199
191
|
for (const stats of sortedAuthors) {
|
|
200
192
|
md += `| ${stats.author} | ${stats.statisticsPeriod || "-"} | ${stats.commits} | ${stats.openspecProposals.size} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
|
|
201
193
|
`;
|
package/dist/cjs/git-analyzer.js
CHANGED
|
@@ -44,13 +44,15 @@ var GitAnalyzer = class {
|
|
|
44
44
|
const untilStr = until.toISOString();
|
|
45
45
|
const logOptions = {
|
|
46
46
|
"--since": sinceStr,
|
|
47
|
-
"--until": untilStr
|
|
48
|
-
"--all": null
|
|
47
|
+
"--until": untilStr
|
|
49
48
|
};
|
|
50
49
|
if (branches.length > 0) {
|
|
50
|
+
delete logOptions["--all"];
|
|
51
51
|
for (const branch of branches) {
|
|
52
|
-
logOptions[
|
|
52
|
+
logOptions[branch] = null;
|
|
53
53
|
}
|
|
54
|
+
} else {
|
|
55
|
+
logOptions["--all"] = null;
|
|
54
56
|
}
|
|
55
57
|
const log = await this.git.log(logOptions);
|
|
56
58
|
const commits = [];
|
|
@@ -83,11 +85,7 @@ var GitAnalyzer = class {
|
|
|
83
85
|
}
|
|
84
86
|
async analyzeCommit(commit) {
|
|
85
87
|
try {
|
|
86
|
-
const show = await this.git.show([
|
|
87
|
-
"--numstat",
|
|
88
|
-
"--format=",
|
|
89
|
-
commit.hash
|
|
90
|
-
]);
|
|
88
|
+
const show = await this.git.show(["--numstat", "--format=", commit.hash]);
|
|
91
89
|
const lines = show.split("\n").filter((line) => line.trim());
|
|
92
90
|
const fileChanges = [];
|
|
93
91
|
const openspecProposals = /* @__PURE__ */ new Set();
|
|
@@ -103,9 +101,7 @@ var GitAnalyzer = class {
|
|
|
103
101
|
const additions = addStr === "-" ? 0 : parseInt(addStr, 10);
|
|
104
102
|
const deletions = delStr === "-" ? 0 : parseInt(delStr, 10);
|
|
105
103
|
if (path.startsWith(openspecDir)) {
|
|
106
|
-
const proposalMatch = path.match(
|
|
107
|
-
new RegExp(`^${openspecDir}changes/([^/]+)`)
|
|
108
|
-
);
|
|
104
|
+
const proposalMatch = path.match(new RegExp(`^${openspecDir}changes/([^/]+)`));
|
|
109
105
|
if (proposalMatch) {
|
|
110
106
|
openspecProposals.add(proposalMatch[1]);
|
|
111
107
|
}
|
|
@@ -123,14 +119,8 @@ var GitAnalyzer = class {
|
|
|
123
119
|
}
|
|
124
120
|
}
|
|
125
121
|
if (openspecProposals.size > 0 && hasCodeChanges) {
|
|
126
|
-
const totalAdditions = fileChanges.reduce(
|
|
127
|
-
|
|
128
|
-
0
|
|
129
|
-
);
|
|
130
|
-
const totalDeletions = fileChanges.reduce(
|
|
131
|
-
(sum, f) => sum + f.deletions,
|
|
132
|
-
0
|
|
133
|
-
);
|
|
122
|
+
const totalAdditions = fileChanges.reduce((sum, f) => sum + f.additions, 0);
|
|
123
|
+
const totalDeletions = fileChanges.reduce((sum, f) => sum + f.deletions, 0);
|
|
134
124
|
return {
|
|
135
125
|
commit,
|
|
136
126
|
openspecProposals,
|
|
@@ -155,10 +145,7 @@ var GitAnalyzer = class {
|
|
|
155
145
|
});
|
|
156
146
|
const authors = /* @__PURE__ */ new Set();
|
|
157
147
|
for (const commit of log.all) {
|
|
158
|
-
const normalizedAuthor = (0, import_config.normalizeAuthor)(
|
|
159
|
-
commit.author_name,
|
|
160
|
-
this.config.authorMapping
|
|
161
|
-
);
|
|
148
|
+
const normalizedAuthor = (0, import_config.normalizeAuthor)(commit.author_name, this.config.authorMapping);
|
|
162
149
|
authors.add(normalizedAuthor);
|
|
163
150
|
}
|
|
164
151
|
return authors;
|
package/dist/cjs/i18n/index.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"cli.option.config": "Configuration file path",
|
|
13
13
|
"cli.option.verbose": "Verbose output mode",
|
|
14
14
|
"cli.option.lang": "Language for output (en, zh-CN)",
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
"loading.config": "🔍 Loading configuration...",
|
|
17
17
|
"loading.activeUsers": "🔍 Fetching active users...",
|
|
18
18
|
"loading.analyzing": "🔍 Analyzing commit history...",
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
"info.timeRange": "📅 Time Range: {{since}} ~ {{until}}",
|
|
21
21
|
"info.branches": "🌿 Branches: {{branches}}",
|
|
22
22
|
"info.allBranches": "All branches",
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"info.foundCommits": "📝 Found {{count}} commits, analyzing...",
|
|
25
25
|
"info.analysisProgress": " Analysis progress: {{current}}/{{total}}",
|
|
26
26
|
"info.qualifyingCommits": "✅ Found {{count}} qualifying commits (containing OpenSpec proposals and code changes)",
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
"warning.noCommits": "⚠️ No commits found matching the criteria",
|
|
29
29
|
"warning.noQualifyingCommits": "⚠️ No commits found containing both OpenSpec proposals and code changes",
|
|
30
30
|
"warning.noBranches": "⚠️ No remote branches found",
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
"error.prefix": "❌ Error:",
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
"branch.fetching": "\n🔍 Fetching active branches...",
|
|
35
35
|
"branch.selectMode": "How would you like to select branches?",
|
|
36
36
|
"branch.mode.select": "Select from active branches",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"branch.customSeparator": "--- Custom input ---",
|
|
43
43
|
"branch.selected": "\n✓ Selected branches:",
|
|
44
44
|
"branch.lastCommit": "last commit: {{date}}",
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
"output.title": "\n📊 OpenSpec Statistics Report\n",
|
|
47
47
|
"output.timeRange": "Time Range: {{since}} ~ {{until}}\n",
|
|
48
48
|
"output.branches": "Branches: {{branches}}\n",
|
|
49
49
|
"output.totalCommits": "Total Commits: {{count}}\n\n",
|
|
50
50
|
"output.proposals": " Proposals: {{proposals}}\n",
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
"table.branch": "Branch",
|
|
53
53
|
"table.period": "Period",
|
|
54
54
|
"table.commits": "Commits",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"table.proposalsList": "Proposals List",
|
|
63
63
|
"table.proposalsCount": "Proposals Count",
|
|
64
64
|
"table.totalDeduplicated": "TOTAL (Deduplicated)",
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
"markdown.title": "# OpenSpec Statistics Report\n\n",
|
|
67
67
|
"markdown.timeRange": "**Time Range**: {{since}} ~ {{until}}\n\n",
|
|
68
68
|
"markdown.branches": "**Branches**: {{branches}}\n\n",
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"cli.option.config": "配置文件路径",
|
|
13
13
|
"cli.option.verbose": "详细输出模式",
|
|
14
14
|
"cli.option.lang": "输出语言(en, zh-CN)",
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
"loading.config": "🔍 正在加载配置...",
|
|
17
17
|
"loading.activeUsers": "🔍 正在获取活跃用户...",
|
|
18
18
|
"loading.analyzing": "🔍 正在分析提交历史...",
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
"info.timeRange": "📅 时间范围:{{since}} ~ {{until}}",
|
|
21
21
|
"info.branches": "🌿 分支:{{branches}}",
|
|
22
22
|
"info.allBranches": "所有分支",
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"info.foundCommits": "📝 找到 {{count}} 个提交,正在分析...",
|
|
25
25
|
"info.analysisProgress": " 分析进度:{{current}}/{{total}}",
|
|
26
26
|
"info.qualifyingCommits": "✅ 找到 {{count}} 个符合条件的提交(包含 OpenSpec 提案和代码变更)",
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
"warning.noCommits": "⚠️ 未找到符合条件的提交",
|
|
29
29
|
"warning.noQualifyingCommits": "⚠️ 未找到同时包含 OpenSpec 提案和代码变更的提交",
|
|
30
30
|
"warning.noBranches": "⚠️ 未找到远程分支",
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
"error.prefix": "❌ 错误:",
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
"branch.fetching": "\n🔍 正在获取活跃分支...",
|
|
35
35
|
"branch.selectMode": "您想如何选择分支?",
|
|
36
36
|
"branch.mode.select": "从活跃分支中选择",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"branch.customSeparator": "--- 自定义输入 ---",
|
|
43
43
|
"branch.selected": "\n✓ 已选择的分支:",
|
|
44
44
|
"branch.lastCommit": "最后提交:{{date}}",
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
"output.title": "\n📊 OpenSpec 统计报告\n",
|
|
47
47
|
"output.timeRange": "时间范围:{{since}} ~ {{until}}\n",
|
|
48
48
|
"output.branches": "分支:{{branches}}\n",
|
|
49
49
|
"output.totalCommits": "总提交数:{{count}}\n\n",
|
|
50
50
|
"output.proposals": " 提案:{{proposals}}\n",
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
"table.branch": "分支",
|
|
53
53
|
"table.period": "周期",
|
|
54
54
|
"table.commits": "提交数",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"table.proposalsList": "提案列表",
|
|
63
63
|
"table.proposalsCount": "提案数量",
|
|
64
64
|
"table.totalDeduplicated": "总计(去重)",
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
"markdown.title": "# OpenSpec 统计报告\n\n",
|
|
67
67
|
"markdown.timeRange": "**时间范围**:{{since}} ~ {{until}}\n\n",
|
|
68
68
|
"markdown.branches": "**分支**:{{branches}}\n\n",
|
|
@@ -31,10 +31,7 @@ var StatsAggregator = class {
|
|
|
31
31
|
aggregate(analyses, since, until, branches, filterAuthor) {
|
|
32
32
|
const authorStatsMap = /* @__PURE__ */ new Map();
|
|
33
33
|
for (const analysis of analyses) {
|
|
34
|
-
const normalizedAuthor = (0, import_config.normalizeAuthor)(
|
|
35
|
-
analysis.commit.author,
|
|
36
|
-
this.config.authorMapping
|
|
37
|
-
);
|
|
34
|
+
const normalizedAuthor = (0, import_config.normalizeAuthor)(analysis.commit.author, this.config.authorMapping);
|
|
38
35
|
if (this.activeAuthors && !this.activeAuthors.has(normalizedAuthor)) {
|
|
39
36
|
continue;
|
|
40
37
|
}
|
|
@@ -103,9 +100,17 @@ var StatsAggregator = class {
|
|
|
103
100
|
stats.statisticsPeriod = days === 0 ? "1 day" : `${days + 1} days`;
|
|
104
101
|
}
|
|
105
102
|
}
|
|
103
|
+
const actualBranches = /* @__PURE__ */ new Set();
|
|
104
|
+
for (const stats of authorStatsMap.values()) {
|
|
105
|
+
if (stats.branchStats) {
|
|
106
|
+
for (const branch of stats.branchStats.keys()) {
|
|
107
|
+
actualBranches.add(branch);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
106
111
|
return {
|
|
107
112
|
timeRange: { since, until },
|
|
108
|
-
branches,
|
|
113
|
+
branches: actualBranches.size > 0 ? Array.from(actualBranches) : branches,
|
|
109
114
|
authors: authorStatsMap,
|
|
110
115
|
totalCommits: analyses.length
|
|
111
116
|
};
|
package/dist/esm/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getDefaultTimeRange, parseDateTime, parseBranches } from "./time-utils.
|
|
|
13
13
|
import { selectBranches } from "./branch-selector.js";
|
|
14
14
|
import { initI18n, t } from "./i18n/index.js";
|
|
15
15
|
var program = new Command();
|
|
16
|
-
program.name('openspec-stat').description(
|
|
16
|
+
program.name('openspec-stat').description("Track team members' OpenSpec proposals and code changes in Git repositories").version('0.0.1').option('-r, --repo <path>', 'Repository path', '.').option('-b, --branches <branches>', 'Branch list, comma-separated').option('--no-interactive', 'Disable interactive branch selection').option('-s, --since <datetime>', 'Start time (default: yesterday 20:00)').option('-u, --until <datetime>', 'End time (default: today 20:00)').option('-a, --author <name>', 'Filter by specific author').option('--json', 'Output in JSON format').option('--csv', 'Output in CSV format').option('--markdown', 'Output in Markdown format').option('-c, --config <path>', 'Configuration file path').option('-v, --verbose', 'Verbose output mode').option('-l, --lang <language>', 'Language for output (en, zh-CN)', 'en').action( /*#__PURE__*/function () {
|
|
17
17
|
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(options) {
|
|
18
18
|
var config, since, until, defaultRange, branches, analyzer, activeAuthors, commits, analyses, i, commit, analysis, aggregator, result, formatter;
|
|
19
19
|
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
package/dist/esm/git-analyzer.js
CHANGED
|
@@ -37,21 +37,23 @@ export var GitAnalyzer = /*#__PURE__*/function () {
|
|
|
37
37
|
untilStr = until.toISOString();
|
|
38
38
|
logOptions = {
|
|
39
39
|
'--since': sinceStr,
|
|
40
|
-
'--until': untilStr
|
|
41
|
-
'--all': null
|
|
40
|
+
'--until': untilStr
|
|
42
41
|
};
|
|
43
42
|
if (branches.length > 0) {
|
|
43
|
+
delete logOptions['--all'];
|
|
44
44
|
_iterator = _createForOfIteratorHelper(branches);
|
|
45
45
|
try {
|
|
46
46
|
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
47
47
|
branch = _step.value;
|
|
48
|
-
logOptions[
|
|
48
|
+
logOptions[branch] = null;
|
|
49
49
|
}
|
|
50
50
|
} catch (err) {
|
|
51
51
|
_iterator.e(err);
|
|
52
52
|
} finally {
|
|
53
53
|
_iterator.f();
|
|
54
54
|
}
|
|
55
|
+
} else {
|
|
56
|
+
logOptions['--all'] = null;
|
|
55
57
|
}
|
|
56
58
|
_context.next = 6;
|
|
57
59
|
return this.git.log(logOptions);
|
package/dist/esm/i18n/index.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"cli.option.config": "Configuration file path",
|
|
13
13
|
"cli.option.verbose": "Verbose output mode",
|
|
14
14
|
"cli.option.lang": "Language for output (en, zh-CN)",
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
"loading.config": "🔍 Loading configuration...",
|
|
17
17
|
"loading.activeUsers": "🔍 Fetching active users...",
|
|
18
18
|
"loading.analyzing": "🔍 Analyzing commit history...",
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
"info.timeRange": "📅 Time Range: {{since}} ~ {{until}}",
|
|
21
21
|
"info.branches": "🌿 Branches: {{branches}}",
|
|
22
22
|
"info.allBranches": "All branches",
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"info.foundCommits": "📝 Found {{count}} commits, analyzing...",
|
|
25
25
|
"info.analysisProgress": " Analysis progress: {{current}}/{{total}}",
|
|
26
26
|
"info.qualifyingCommits": "✅ Found {{count}} qualifying commits (containing OpenSpec proposals and code changes)",
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
"warning.noCommits": "⚠️ No commits found matching the criteria",
|
|
29
29
|
"warning.noQualifyingCommits": "⚠️ No commits found containing both OpenSpec proposals and code changes",
|
|
30
30
|
"warning.noBranches": "⚠️ No remote branches found",
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
"error.prefix": "❌ Error:",
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
"branch.fetching": "\n🔍 Fetching active branches...",
|
|
35
35
|
"branch.selectMode": "How would you like to select branches?",
|
|
36
36
|
"branch.mode.select": "Select from active branches",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"branch.customSeparator": "--- Custom input ---",
|
|
43
43
|
"branch.selected": "\n✓ Selected branches:",
|
|
44
44
|
"branch.lastCommit": "last commit: {{date}}",
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
"output.title": "\n📊 OpenSpec Statistics Report\n",
|
|
47
47
|
"output.timeRange": "Time Range: {{since}} ~ {{until}}\n",
|
|
48
48
|
"output.branches": "Branches: {{branches}}\n",
|
|
49
49
|
"output.totalCommits": "Total Commits: {{count}}\n\n",
|
|
50
50
|
"output.proposals": " Proposals: {{proposals}}\n",
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
"table.branch": "Branch",
|
|
53
53
|
"table.period": "Period",
|
|
54
54
|
"table.commits": "Commits",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"table.proposalsList": "Proposals List",
|
|
63
63
|
"table.proposalsCount": "Proposals Count",
|
|
64
64
|
"table.totalDeduplicated": "TOTAL (Deduplicated)",
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
"markdown.title": "# OpenSpec Statistics Report\n\n",
|
|
67
67
|
"markdown.timeRange": "**Time Range**: {{since}} ~ {{until}}\n\n",
|
|
68
68
|
"markdown.branches": "**Branches**: {{branches}}\n\n",
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"cli.option.config": "配置文件路径",
|
|
13
13
|
"cli.option.verbose": "详细输出模式",
|
|
14
14
|
"cli.option.lang": "输出语言(en, zh-CN)",
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
"loading.config": "🔍 正在加载配置...",
|
|
17
17
|
"loading.activeUsers": "🔍 正在获取活跃用户...",
|
|
18
18
|
"loading.analyzing": "🔍 正在分析提交历史...",
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
"info.timeRange": "📅 时间范围:{{since}} ~ {{until}}",
|
|
21
21
|
"info.branches": "🌿 分支:{{branches}}",
|
|
22
22
|
"info.allBranches": "所有分支",
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"info.foundCommits": "📝 找到 {{count}} 个提交,正在分析...",
|
|
25
25
|
"info.analysisProgress": " 分析进度:{{current}}/{{total}}",
|
|
26
26
|
"info.qualifyingCommits": "✅ 找到 {{count}} 个符合条件的提交(包含 OpenSpec 提案和代码变更)",
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
"warning.noCommits": "⚠️ 未找到符合条件的提交",
|
|
29
29
|
"warning.noQualifyingCommits": "⚠️ 未找到同时包含 OpenSpec 提案和代码变更的提交",
|
|
30
30
|
"warning.noBranches": "⚠️ 未找到远程分支",
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
"error.prefix": "❌ 错误:",
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
"branch.fetching": "\n🔍 正在获取活跃分支...",
|
|
35
35
|
"branch.selectMode": "您想如何选择分支?",
|
|
36
36
|
"branch.mode.select": "从活跃分支中选择",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"branch.customSeparator": "--- 自定义输入 ---",
|
|
43
43
|
"branch.selected": "\n✓ 已选择的分支:",
|
|
44
44
|
"branch.lastCommit": "最后提交:{{date}}",
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
"output.title": "\n📊 OpenSpec 统计报告\n",
|
|
47
47
|
"output.timeRange": "时间范围:{{since}} ~ {{until}}\n",
|
|
48
48
|
"output.branches": "分支:{{branches}}\n",
|
|
49
49
|
"output.totalCommits": "总提交数:{{count}}\n\n",
|
|
50
50
|
"output.proposals": " 提案:{{proposals}}\n",
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
"table.branch": "分支",
|
|
53
53
|
"table.period": "周期",
|
|
54
54
|
"table.commits": "提交数",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"table.proposalsList": "提案列表",
|
|
63
63
|
"table.proposalsCount": "提案数量",
|
|
64
64
|
"table.totalDeduplicated": "总计(去重)",
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
"markdown.title": "# OpenSpec 统计报告\n\n",
|
|
67
67
|
"markdown.timeRange": "**时间范围**:{{since}} ~ {{until}}\n\n",
|
|
68
68
|
"markdown.branches": "**分支**:{{branches}}\n\n",
|
|
@@ -52,17 +52,17 @@ export var StatsAggregator = /*#__PURE__*/function () {
|
|
|
52
52
|
stats.deletions += analysis.totalDeletions;
|
|
53
53
|
stats.netChanges += analysis.netChanges;
|
|
54
54
|
stats.codeFilesChanged += analysis.codeFiles.length;
|
|
55
|
-
var
|
|
56
|
-
|
|
55
|
+
var _iterator4 = _createForOfIteratorHelper(analysis.openspecProposals),
|
|
56
|
+
_step4;
|
|
57
57
|
try {
|
|
58
|
-
for (
|
|
59
|
-
var _proposal =
|
|
58
|
+
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
|
|
59
|
+
var _proposal = _step4.value;
|
|
60
60
|
stats.openspecProposals.add(_proposal);
|
|
61
61
|
}
|
|
62
62
|
} catch (err) {
|
|
63
|
-
|
|
63
|
+
_iterator4.e(err);
|
|
64
64
|
} finally {
|
|
65
|
-
|
|
65
|
+
_iterator4.f();
|
|
66
66
|
}
|
|
67
67
|
if (!stats.lastCommitDate || analysis.commit.date > stats.lastCommitDate) {
|
|
68
68
|
stats.lastCommitDate = analysis.commit.date;
|
|
@@ -71,11 +71,11 @@ export var StatsAggregator = /*#__PURE__*/function () {
|
|
|
71
71
|
stats.firstCommitDate = analysis.commit.date;
|
|
72
72
|
}
|
|
73
73
|
if (analysis.commit.branches && stats.branchStats) {
|
|
74
|
-
var
|
|
75
|
-
|
|
74
|
+
var _iterator5 = _createForOfIteratorHelper(analysis.commit.branches),
|
|
75
|
+
_step5;
|
|
76
76
|
try {
|
|
77
|
-
for (
|
|
78
|
-
var branch =
|
|
77
|
+
for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
|
|
78
|
+
var branch = _step5.value;
|
|
79
79
|
var branchStat = stats.branchStats.get(branch);
|
|
80
80
|
if (!branchStat) {
|
|
81
81
|
branchStat = {
|
|
@@ -94,23 +94,23 @@ export var StatsAggregator = /*#__PURE__*/function () {
|
|
|
94
94
|
branchStat.deletions += analysis.totalDeletions;
|
|
95
95
|
branchStat.netChanges += analysis.netChanges;
|
|
96
96
|
branchStat.codeFilesChanged += analysis.codeFiles.length;
|
|
97
|
-
var
|
|
98
|
-
|
|
97
|
+
var _iterator6 = _createForOfIteratorHelper(analysis.openspecProposals),
|
|
98
|
+
_step6;
|
|
99
99
|
try {
|
|
100
|
-
for (
|
|
101
|
-
var proposal =
|
|
100
|
+
for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
|
|
101
|
+
var proposal = _step6.value;
|
|
102
102
|
branchStat.openspecProposals.add(proposal);
|
|
103
103
|
}
|
|
104
104
|
} catch (err) {
|
|
105
|
-
|
|
105
|
+
_iterator6.e(err);
|
|
106
106
|
} finally {
|
|
107
|
-
|
|
107
|
+
_iterator6.f();
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
} catch (err) {
|
|
111
|
-
|
|
111
|
+
_iterator5.e(err);
|
|
112
112
|
} finally {
|
|
113
|
-
|
|
113
|
+
_iterator5.f();
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
}
|
|
@@ -134,12 +134,38 @@ export var StatsAggregator = /*#__PURE__*/function () {
|
|
|
134
134
|
} finally {
|
|
135
135
|
_iterator2.f();
|
|
136
136
|
}
|
|
137
|
+
var actualBranches = new Set();
|
|
138
|
+
var _iterator3 = _createForOfIteratorHelper(authorStatsMap.values()),
|
|
139
|
+
_step3;
|
|
140
|
+
try {
|
|
141
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
142
|
+
var _stats2 = _step3.value;
|
|
143
|
+
if (_stats2.branchStats) {
|
|
144
|
+
var _iterator7 = _createForOfIteratorHelper(_stats2.branchStats.keys()),
|
|
145
|
+
_step7;
|
|
146
|
+
try {
|
|
147
|
+
for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
|
|
148
|
+
var _branch = _step7.value;
|
|
149
|
+
actualBranches.add(_branch);
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
_iterator7.e(err);
|
|
153
|
+
} finally {
|
|
154
|
+
_iterator7.f();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
_iterator3.e(err);
|
|
160
|
+
} finally {
|
|
161
|
+
_iterator3.f();
|
|
162
|
+
}
|
|
137
163
|
return {
|
|
138
164
|
timeRange: {
|
|
139
165
|
since: since,
|
|
140
166
|
until: until
|
|
141
167
|
},
|
|
142
|
-
branches: branches,
|
|
168
|
+
branches: actualBranches.size > 0 ? Array.from(actualBranches) : branches,
|
|
143
169
|
authors: authorStatsMap,
|
|
144
170
|
totalCommits: analyses.length
|
|
145
171
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openspec-stat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Track team members' OpenSpec proposals and code changes in Git repositories",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"types": "dist/cjs/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"packageManager": "pnpm@10.12.1",
|
|
8
9
|
"bin": {
|
|
9
10
|
"openspec-stat": "dist/esm/cli.js"
|
|
10
11
|
},
|
|
@@ -14,7 +15,20 @@
|
|
|
14
15
|
"build:deps": "father prebundle",
|
|
15
16
|
"prepublishOnly": "father doctor && npm run build",
|
|
16
17
|
"test:cli": "node dist/esm/cli.js --help",
|
|
17
|
-
"link:global": "npm link"
|
|
18
|
+
"link:global": "npm link",
|
|
19
|
+
"link:local": "pnpm run build && chmod +x dist/esm/cli.js && npm link .",
|
|
20
|
+
"unlink": "npm unlink -g openspec-stat",
|
|
21
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
22
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
23
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
|
|
24
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
|
|
25
|
+
"prepare": "husky",
|
|
26
|
+
"changeset": "changeset",
|
|
27
|
+
"changeset:add": "changeset add",
|
|
28
|
+
"version": "changeset version && git add . && git commit -m 'chore: version packages'",
|
|
29
|
+
"release": "pnpm run release:version && pnpm run release:tag",
|
|
30
|
+
"release:version": "pnpm changeset version && git add . && git commit -m 'chore: version packages' && git push origin main",
|
|
31
|
+
"release:tag": "git tag v$(node -p \"require('./package.json').version\") && git push origin v$(node -p \"require('./package.json').version\")"
|
|
18
32
|
},
|
|
19
33
|
"keywords": [
|
|
20
34
|
"openspec",
|
|
@@ -43,8 +57,26 @@
|
|
|
43
57
|
"access": "public"
|
|
44
58
|
},
|
|
45
59
|
"devDependencies": {
|
|
60
|
+
"@changesets/cli": "^2.27.1",
|
|
46
61
|
"@types/node": "^25.0.3",
|
|
47
|
-
"
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
63
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
64
|
+
"eslint": "^8.56.0",
|
|
65
|
+
"eslint-config-prettier": "^9.1.0",
|
|
66
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
67
|
+
"father": "^4.6.13",
|
|
68
|
+
"husky": "^9.0.11",
|
|
69
|
+
"lint-staged": "^15.2.2",
|
|
70
|
+
"prettier": "^3.2.5"
|
|
71
|
+
},
|
|
72
|
+
"lint-staged": {
|
|
73
|
+
"*.{ts,tsx}": [
|
|
74
|
+
"eslint --fix",
|
|
75
|
+
"prettier --write"
|
|
76
|
+
],
|
|
77
|
+
"*.{js,jsx,json,md}": [
|
|
78
|
+
"prettier --write"
|
|
79
|
+
]
|
|
48
80
|
},
|
|
49
81
|
"dependencies": {
|
|
50
82
|
"@inquirer/prompts": "^8.1.0",
|