openspec-stat 1.4.0 → 1.4.1
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.
|
|
7
|
+
program.name('openspec-stat').description("Track team members' OpenSpec proposals and code changes in Git repositories").version("1.4.1").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
|
-
|
|
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('
|
|
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.
|
|
88
|
-
"multi.repo.
|
|
89
|
-
"multi.repo.
|
|
90
|
-
"multi.repo.
|
|
91
|
-
"multi.repo.
|
|
92
|
-
"multi.repo.
|
|
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": "
|
|
99
|
+
"multi.summary.title": "Summary",
|
|
97
100
|
"multi.summary.repos": "Repositories: {{total}} ({{success}} succeeded, {{failed}} failed)",
|
|
98
|
-
"multi.
|
|
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.
|
|
88
|
-
"multi.repo.
|
|
89
|
-
"multi.repo.
|
|
90
|
-
"multi.repo.
|
|
91
|
-
"multi.repo.
|
|
92
|
-
"multi.repo.
|
|
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": "
|
|
99
|
+
"multi.summary.title": "汇总",
|
|
97
100
|
"multi.summary.repos": "仓库:{{total}} 个({{success}} 成功,{{failed}} 失败)",
|
|
98
|
-
"multi.
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -147,17 +149,25 @@ export class MultiRepoAnalyzer {
|
|
|
147
149
|
}
|
|
148
150
|
async processInBatches(items, processor, concurrency) {
|
|
149
151
|
const results = [];
|
|
152
|
+
const divider = '-'.repeat(64);
|
|
150
153
|
for (let i = 0; i < items.length; i += concurrency) {
|
|
151
154
|
const batch = items.slice(i, i + concurrency);
|
|
152
155
|
const batchNumber = Math.floor(i / concurrency) + 1;
|
|
153
156
|
const totalBatches = Math.ceil(items.length / concurrency);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
157
|
+
console.log(chalk.gray(divider));
|
|
158
|
+
console.log(chalk.gray(t('multi.progress.batch', {
|
|
159
|
+
current: String(batchNumber),
|
|
160
|
+
total: String(totalBatches),
|
|
161
|
+
count: String(batch.length)
|
|
162
|
+
})));
|
|
163
|
+
console.log(chalk.gray(divider));
|
|
164
|
+
const batchResults = await Promise.all(batch.map((item, index) => processor(item, {
|
|
165
|
+
batchNumber,
|
|
166
|
+
totalBatches,
|
|
167
|
+
indexInBatch: index,
|
|
168
|
+
batchSize: batch.length,
|
|
169
|
+
totalItems: items.length
|
|
170
|
+
})));
|
|
161
171
|
results.push(...batchResults);
|
|
162
172
|
}
|
|
163
173
|
return results;
|
|
@@ -219,7 +229,7 @@ export class MultiRepoAnalyzer {
|
|
|
219
229
|
return ` (${order}/${this.totalCloneTargets})`;
|
|
220
230
|
}
|
|
221
231
|
reportCloneStatus(status, repoName, progressSuffix, spinner, errorMessage) {
|
|
222
|
-
const messageKey = status === 'start' ? 'multi.repo.cloning' : status === 'success' ? 'multi.repo.cloned' : 'multi.repo.
|
|
232
|
+
const messageKey = status === 'start' ? 'multi.repo.cloning' : status === 'success' ? 'multi.repo.cloned' : 'multi.repo.cloneFailed';
|
|
223
233
|
const messageParams = {
|
|
224
234
|
repo: repoName
|
|
225
235
|
};
|