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 CHANGED
@@ -3,6 +3,8 @@
3
3
  [![NPM version](https://img.shields.io/npm/v/openspec-stat.svg?style=flat)](https://npmjs.com/package/openspec-stat)
4
4
  [![NPM downloads](http://img.shields.io/npm/dm/openspec-stat.svg?style=flat)](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
- config.defaultSinceHours,
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(import_chalk.default.blue((0, import_i18n.t)("info.branches", {
77
- branches: branches.join(", ") || (0, import_i18n.t)("info.allBranches")
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((0, import_i18n.t)("info.analysisProgress", {
107
- current: String(i + 1),
108
- total: String(commits.length)
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));
@@ -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
  `;
@@ -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[`--remotes=${branch}`] = null;
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
- (sum, f) => sum + f.additions,
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;
@@ -33,7 +33,7 @@ var __filename = (0, import_url.fileURLToPath)(import_meta.url);
33
33
  var __dirname = (0, import_path.dirname)(__filename);
34
34
  var currentLanguage = "en";
35
35
  var translations = {
36
- "en": {},
36
+ en: {},
37
37
  "zh-CN": {}
38
38
  };
39
39
  function loadTranslations() {
@@ -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('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 () {
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) {
@@ -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["--remotes=".concat(branch)] = null;
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);
@@ -5,7 +5,7 @@ var __filename = fileURLToPath(import.meta.url);
5
5
  var __dirname = dirname(__filename);
6
6
  var currentLanguage = 'en';
7
7
  var translations = {
8
- 'en': {},
8
+ en: {},
9
9
  'zh-CN': {}
10
10
  };
11
11
  function loadTranslations() {
@@ -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 _iterator3 = _createForOfIteratorHelper(analysis.openspecProposals),
56
- _step3;
55
+ var _iterator4 = _createForOfIteratorHelper(analysis.openspecProposals),
56
+ _step4;
57
57
  try {
58
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
59
- var _proposal = _step3.value;
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
- _iterator3.e(err);
63
+ _iterator4.e(err);
64
64
  } finally {
65
- _iterator3.f();
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 _iterator4 = _createForOfIteratorHelper(analysis.commit.branches),
75
- _step4;
74
+ var _iterator5 = _createForOfIteratorHelper(analysis.commit.branches),
75
+ _step5;
76
76
  try {
77
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
78
- var branch = _step4.value;
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 _iterator5 = _createForOfIteratorHelper(analysis.openspecProposals),
98
- _step5;
97
+ var _iterator6 = _createForOfIteratorHelper(analysis.openspecProposals),
98
+ _step6;
99
99
  try {
100
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
101
- var proposal = _step5.value;
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
- _iterator5.e(err);
105
+ _iterator6.e(err);
106
106
  } finally {
107
- _iterator5.f();
107
+ _iterator6.f();
108
108
  }
109
109
  }
110
110
  } catch (err) {
111
- _iterator4.e(err);
111
+ _iterator5.e(err);
112
112
  } finally {
113
- _iterator4.f();
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.0.0",
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
- "father": "^4.6.13"
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",