openspec-stat 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/cli.js CHANGED
@@ -4,7 +4,7 @@ import { runSingleRepoCommand } from "./commands/single.js";
4
4
  import { runMultiRepoCommand } from "./commands/multi.js";
5
5
  import { runInitCommand } from "./commands/init.js";
6
6
  const program = new Command();
7
- program.name('openspec-stat').description("Track team members' OpenSpec proposals and code changes in Git repositories").version("1.4.0").enablePositionalOptions().passThroughOptions();
7
+ program.name('openspec-stat').description("Track team members' OpenSpec proposals and code changes in Git repositories").version("1.4.2").enablePositionalOptions().passThroughOptions();
8
8
 
9
9
  // Default command for single-repository mode (for backward compatibility)
10
10
  program.argument('[repo]', 'Repository path', '.').option('-r, --repo <path>', 'Repository path (alternative)', '.').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').option('--no-fetch', 'Skip fetching remote branches').action(async (repo, options) => {
@@ -68,13 +68,17 @@ export async function runMultiRepoCommand(options) {
68
68
  const repoResults = await analyzer.analyzeAll(since, until);
69
69
  const successResults = repoResults.filter(r => r.success);
70
70
  const failedResults = repoResults.filter(r => !r.success);
71
- console.log(chalk.blue(t('multi.summary.title') + t('multi.summary.repos', {
71
+ const summaryDivider = '-'.repeat(64);
72
+ console.log(chalk.gray(summaryDivider));
73
+ console.log(chalk.blue(t('multi.summary.title')));
74
+ console.log(chalk.gray(summaryDivider));
75
+ console.log(chalk.blue(t('multi.summary.repos', {
72
76
  total: String(repoResults.length),
73
77
  success: String(successResults.length),
74
78
  failed: String(failedResults.length)
75
79
  })));
76
80
  if (failedResults.length > 0) {
77
- console.log(chalk.yellow('\nFailed repositories:'));
81
+ console.log(chalk.yellow(`\n${t('multi.summary.failedTitle')}`));
78
82
  failedResults.forEach(r => {
79
83
  console.log(chalk.red(` - ${r.repository}: ${r.error}`));
80
84
  });
@@ -84,18 +84,22 @@
84
84
  "multi.beta.warning": "BETA: Multi-repository mode is experimental",
85
85
  "multi.beta.feedback": " Please report issues at: https://github.com/Orchardxyz/openspec-stat/issues",
86
86
  "multi.loading.config": "Loading multi-repository configuration...",
87
- "multi.repo.cloning": "Cloning {{repo}}...",
88
- "multi.repo.cloned": "Successfully cloned {{repo}}",
89
- "multi.repo.fetching": "Fetching remote branches for {{repo}}...",
90
- "multi.repo.analyzing": "Analyzing {{repo}} ({{type}})...",
91
- "multi.repo.completed": "Completed {{repo}}: {{commits}} commits",
92
- "multi.repo.failed": "Failed {{repo}}: {{error}}",
87
+ "multi.repo.header": " [{{current}}/{{total}}] {{repo}}{{typeSuffix}}",
88
+ "multi.repo.type.remote": " (remote)",
89
+ "multi.repo.cloning": " - Cloning {{repo}}...",
90
+ "multi.repo.cloned": " - Clone completed: {{repo}}",
91
+ "multi.repo.cloneFailed": " - Clone failed: {{repo}} ({{error}})",
92
+ "multi.repo.fetching": " - Fetching remote branches...",
93
+ "multi.repo.analyzing": " - Analyzing commits...",
94
+ "multi.repo.completed": " Completed {{repo}}: {{commits}} commits",
95
+ "multi.repo.failed": " Failed {{repo}}: {{error}}",
93
96
  "multi.repo.skipped": "Skipped {{repo}}: disabled",
94
97
  "multi.cleanup.start": "Cleaning up temporary directories...",
95
98
  "multi.cleanup.done": "Cleanup completed",
96
- "multi.summary.title": "\nMulti-Repository Summary\n",
99
+ "multi.summary.title": "Summary",
97
100
  "multi.summary.repos": "Repositories: {{total}} ({{success}} succeeded, {{failed}} failed)",
98
- "multi.progress.batch": "Processing batch {{current}}/{{total}}...",
101
+ "multi.summary.failedTitle": "Failed repositories:",
102
+ "multi.progress.batch": "Processing batch {{current}}/{{total}} ({{count}} repositories)...",
99
103
  "multi.table.repository": "Repository",
100
104
  "multi.table.type": "Type",
101
105
 
@@ -84,18 +84,22 @@
84
84
  "multi.beta.warning": "测试版:多仓库模式为实验性功能",
85
85
  "multi.beta.feedback": " 请反馈问题至:https://github.com/Orchardxyz/openspec-stat/issues",
86
86
  "multi.loading.config": "正在加载多仓库配置...",
87
- "multi.repo.cloning": "正在克隆 {{repo}}...",
88
- "multi.repo.cloned": "成功克隆 {{repo}}",
89
- "multi.repo.fetching": "正在拉取 {{repo}} 的远程分支...",
90
- "multi.repo.analyzing": "正在分析 {{repo}} ({{type}})...",
91
- "multi.repo.completed": "完成 {{repo}}{{commits}} 次提交",
92
- "multi.repo.failed": "失败 {{repo}}:{{error}}",
87
+ "multi.repo.header": " [{{current}}/{{total}}] {{repo}}{{typeSuffix}}",
88
+ "multi.repo.type.remote": "(远程)",
89
+ "multi.repo.cloning": " - 正在克隆 {{repo}}...",
90
+ "multi.repo.cloned": " - 克隆完成:{{repo}}",
91
+ "multi.repo.cloneFailed": " - 克隆失败:{{repo}}{{error}}",
92
+ "multi.repo.fetching": " - 正在拉取远程分支...",
93
+ "multi.repo.analyzing": " - 正在分析提交...",
94
+ "multi.repo.completed": " 完成 {{repo}}:{{commits}} 次提交",
95
+ "multi.repo.failed": " 失败 {{repo}}:{{error}}",
93
96
  "multi.repo.skipped": "跳过 {{repo}}:已禁用",
94
97
  "multi.cleanup.start": "正在清理临时目录...",
95
98
  "multi.cleanup.done": "清理完成",
96
- "multi.summary.title": "\n多仓库汇总\n",
99
+ "multi.summary.title": "汇总",
97
100
  "multi.summary.repos": "仓库:{{total}} 个({{success}} 成功,{{failed}} 失败)",
98
- "multi.progress.batch": "正在处理批次 {{current}}/{{total}}...",
101
+ "multi.summary.failedTitle": "失败的仓库:",
102
+ "multi.progress.batch": "正在处理批次 {{current}}/{{total}}({{count}} 个仓库)...",
99
103
  "multi.table.repository": "仓库",
100
104
  "multi.table.type": "类型",
101
105
 
@@ -24,7 +24,7 @@ export class MultiRepoAnalyzer {
24
24
  this.nextCloneIndex = 1;
25
25
  this.totalCloneTargets = enabledRepos.filter(repo => repo.type === 'remote').length;
26
26
  try {
27
- const results = await this.processInBatches(enabledRepos, repo => this.analyzeRepository(repo, since, until), this.config.parallelism?.maxConcurrent || 3);
27
+ const results = await this.processInBatches(enabledRepos, (repo, context) => this.analyzeRepository(repo, since, until, context), this.config.parallelism?.maxConcurrent || 3);
28
28
  return results;
29
29
  } finally {
30
30
  if (this.config.remoteCache?.cleanupOnComplete) {
@@ -32,7 +32,7 @@ export class MultiRepoAnalyzer {
32
32
  }
33
33
  }
34
34
  }
35
- async analyzeRepository(repo, since, until) {
35
+ async analyzeRepository(repo, since, until, context) {
36
36
  let repoPath;
37
37
  try {
38
38
  if (repo.enabled === false) {
@@ -48,9 +48,12 @@ export class MultiRepoAnalyzer {
48
48
  error: 'disabled'
49
49
  };
50
50
  }
51
- console.log(chalk.blue(t('multi.repo.analyzing', {
51
+ const typeSuffix = repo.type === 'remote' ? t('multi.repo.type.remote') : '';
52
+ console.log(chalk.blue(t('multi.repo.header', {
53
+ current: String(context.indexInBatch + 1),
54
+ total: String(context.batchSize),
52
55
  repo: repo.name,
53
- type: repo.type
56
+ typeSuffix
54
57
  })));
55
58
  if (repo.type === 'local') {
56
59
  repoPath = this.resolveLocalPath(repo.path);
@@ -64,11 +67,10 @@ export class MultiRepoAnalyzer {
64
67
 
65
68
  // Fetch remote branches for local repositories to ensure data is up-to-date
66
69
  if (repo.type === 'local' && this.config.autoFetch !== false) {
67
- console.log(chalk.cyan(t('multi.repo.fetching', {
68
- repo: repo.name
69
- })));
70
+ console.log(chalk.cyan(t('multi.repo.fetching')));
70
71
  await analyzer.fetchRemote();
71
72
  }
73
+ console.log(chalk.gray(t('multi.repo.analyzing')));
72
74
  const commits = await analyzer.getCommits(since, until, repo.branches);
73
75
  const analyses = [];
74
76
  for (const commit of commits) {
@@ -120,8 +122,29 @@ export class MultiRepoAnalyzer {
120
122
  const progressSuffix = this.getProgressSuffix(repo.name);
121
123
  const cloneSpinner = this.isQuiet ? undefined : new SpinnerManager(false);
122
124
  this.reportCloneStatus('start', repo.name, progressSuffix, cloneSpinner);
123
- const git = simpleGit();
125
+ const progressReporter = ({
126
+ method,
127
+ stage,
128
+ progress
129
+ }) => {
130
+ if (this.isQuiet) return;
131
+ const pct = Number.isFinite(progress) ? `${progress.toFixed(1)}%` : '';
132
+ const text = `${t('multi.repo.cloning', {
133
+ repo: repo.name
134
+ })}${progressSuffix} ${method} ${stage}${pct ? ` ${pct}` : ''}`;
135
+ if (cloneSpinner) {
136
+ cloneSpinner.update(text);
137
+ } else {
138
+ console.log(chalk.cyan(text));
139
+ }
140
+ };
141
+ const git = simpleGit({
142
+ progress: progressReporter
143
+ });
124
144
  const cloneArgs = [];
145
+
146
+ // Enable git's progress output so simple-git can emit progress events
147
+ cloneArgs.push('--progress');
125
148
  if (repo.cloneOptions?.depth !== null && repo.cloneOptions?.depth !== undefined) {
126
149
  cloneArgs.push(`--depth=${repo.cloneOptions.depth}`);
127
150
  }
@@ -147,17 +170,25 @@ export class MultiRepoAnalyzer {
147
170
  }
148
171
  async processInBatches(items, processor, concurrency) {
149
172
  const results = [];
173
+ const divider = '-'.repeat(64);
150
174
  for (let i = 0; i < items.length; i += concurrency) {
151
175
  const batch = items.slice(i, i + concurrency);
152
176
  const batchNumber = Math.floor(i / concurrency) + 1;
153
177
  const totalBatches = Math.ceil(items.length / concurrency);
154
- if (totalBatches > 1) {
155
- console.log(chalk.gray(t('multi.progress.batch', {
156
- current: String(batchNumber),
157
- total: String(totalBatches)
158
- })));
159
- }
160
- const batchResults = await Promise.all(batch.map(processor));
178
+ console.log(chalk.gray(divider));
179
+ console.log(chalk.gray(t('multi.progress.batch', {
180
+ current: String(batchNumber),
181
+ total: String(totalBatches),
182
+ count: String(batch.length)
183
+ })));
184
+ console.log(chalk.gray(divider));
185
+ const batchResults = await Promise.all(batch.map((item, index) => processor(item, {
186
+ batchNumber,
187
+ totalBatches,
188
+ indexInBatch: index,
189
+ batchSize: batch.length,
190
+ totalItems: items.length
191
+ })));
161
192
  results.push(...batchResults);
162
193
  }
163
194
  return results;
@@ -219,7 +250,7 @@ export class MultiRepoAnalyzer {
219
250
  return ` (${order}/${this.totalCloneTargets})`;
220
251
  }
221
252
  reportCloneStatus(status, repoName, progressSuffix, spinner, errorMessage) {
222
- const messageKey = status === 'start' ? 'multi.repo.cloning' : status === 'success' ? 'multi.repo.cloned' : 'multi.repo.failed';
253
+ const messageKey = status === 'start' ? 'multi.repo.cloning' : status === 'success' ? 'multi.repo.cloned' : 'multi.repo.cloneFailed';
223
254
  const messageParams = {
224
255
  repo: repoName
225
256
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openspec-stat",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Track team members' OpenSpec proposals and code changes in Git repositories",
5
5
  "type": "module",
6
6
  "main": "dist/esm/index.js",