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.
|
|
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
|
-
|
|
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) {
|
|
@@ -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
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
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.
|
|
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
|
};
|