openspec-stat 1.2.0 → 1.3.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/README.md +28 -0
- package/README.zh-CN.md +28 -0
- package/dist/cjs/cli.js +12 -127
- package/dist/cjs/commands/init.d.ts +7 -0
- package/dist/cjs/commands/init.js +58 -0
- package/dist/cjs/commands/multi.d.ts +16 -0
- package/dist/cjs/commands/multi.js +172 -0
- package/dist/cjs/commands/single.d.ts +2 -0
- package/dist/cjs/commands/single.js +148 -0
- package/dist/cjs/formatters.d.ts +4 -4
- package/dist/cjs/formatters.js +128 -36
- package/dist/cjs/git-analyzer.d.ts +1 -0
- package/dist/cjs/git-analyzer.js +6 -0
- package/dist/cjs/i18n/locales/en.json +74 -1
- package/dist/cjs/i18n/locales/zh-CN.json +74 -1
- package/dist/cjs/multi/config-validator.d.ts +3 -0
- package/dist/cjs/multi/config-validator.js +130 -0
- package/dist/cjs/multi/config-wizard.d.ts +50 -0
- package/dist/cjs/multi/config-wizard.js +331 -0
- package/dist/cjs/multi/multi-repo-analyzer.d.ts +14 -0
- package/dist/cjs/multi/multi-repo-analyzer.js +210 -0
- package/dist/cjs/types.d.ts +46 -0
- package/dist/esm/cli.js +54 -139
- package/dist/esm/commands/init.d.ts +7 -0
- package/dist/esm/commands/init.js +49 -0
- package/dist/esm/commands/multi.d.ts +16 -0
- package/dist/esm/commands/multi.js +192 -0
- package/dist/esm/commands/single.d.ts +2 -0
- package/dist/esm/commands/single.js +162 -0
- package/dist/esm/formatters.d.ts +4 -4
- package/dist/esm/formatters.js +173 -52
- package/dist/esm/git-analyzer.d.ts +1 -0
- package/dist/esm/git-analyzer.js +104 -77
- package/dist/esm/i18n/locales/en.json +74 -1
- package/dist/esm/i18n/locales/zh-CN.json +74 -1
- package/dist/esm/multi/config-validator.d.ts +3 -0
- package/dist/esm/multi/config-validator.js +109 -0
- package/dist/esm/multi/config-wizard.d.ts +50 -0
- package/dist/esm/multi/config-wizard.js +535 -0
- package/dist/esm/multi/multi-repo-analyzer.d.ts +14 -0
- package/dist/esm/multi/multi-repo-analyzer.js +446 -0
- package/dist/esm/types.d.ts +46 -0
- package/package.json +1 -1
package/dist/cjs/formatters.js
CHANGED
|
@@ -36,7 +36,7 @@ var import_cli_table3 = __toESM(require("cli-table3"));
|
|
|
36
36
|
var import_chalk = __toESM(require("chalk"));
|
|
37
37
|
var import_i18n = require("./i18n/index.js");
|
|
38
38
|
var OutputFormatter = class {
|
|
39
|
-
formatTable(result, verbose = false) {
|
|
39
|
+
formatTable(result, verbose = false, showContributors = true) {
|
|
40
40
|
let output = "";
|
|
41
41
|
output += import_chalk.default.bold((0, import_i18n.t)("output.title"));
|
|
42
42
|
output += import_chalk.default.gray(
|
|
@@ -101,6 +101,43 @@ ${(0, import_i18n.t)("output.proposalSummary")}
|
|
|
101
101
|
${(0, import_i18n.t)("output.authorSummary")}
|
|
102
102
|
`);
|
|
103
103
|
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
104
|
+
if (!showContributors) {
|
|
105
|
+
const totalAuthors = sortedAuthors.length;
|
|
106
|
+
const totalCommits = sortedAuthors.reduce((sum, s) => sum + s.commits, 0);
|
|
107
|
+
const totalProposalsSet = /* @__PURE__ */ new Set();
|
|
108
|
+
sortedAuthors.forEach((s) => s.openspecProposals.forEach((p) => totalProposalsSet.add(p)));
|
|
109
|
+
const totalFiles = sortedAuthors.reduce((sum, s) => sum + s.codeFilesChanged, 0);
|
|
110
|
+
const totalAdditions = sortedAuthors.reduce((sum, s) => sum + s.additions, 0);
|
|
111
|
+
const totalDeletions = sortedAuthors.reduce((sum, s) => sum + s.deletions, 0);
|
|
112
|
+
const totalNetChanges = sortedAuthors.reduce((sum, s) => sum + s.netChanges, 0);
|
|
113
|
+
const summaryTable = new import_cli_table3.default({
|
|
114
|
+
head: [
|
|
115
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.contributors")),
|
|
116
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.commits")),
|
|
117
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.proposals")),
|
|
118
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.codeFiles")),
|
|
119
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.additions")),
|
|
120
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.deletions")),
|
|
121
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.netChanges"))
|
|
122
|
+
],
|
|
123
|
+
style: {
|
|
124
|
+
head: [],
|
|
125
|
+
border: []
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
summaryTable.push([
|
|
129
|
+
totalAuthors.toString(),
|
|
130
|
+
totalCommits.toString(),
|
|
131
|
+
totalProposalsSet.size.toString(),
|
|
132
|
+
totalFiles.toString(),
|
|
133
|
+
import_chalk.default.green(`+${totalAdditions}`),
|
|
134
|
+
import_chalk.default.red(`-${totalDeletions}`),
|
|
135
|
+
totalNetChanges >= 0 ? import_chalk.default.green(`+${totalNetChanges}`) : import_chalk.default.red(`${totalNetChanges}`)
|
|
136
|
+
]);
|
|
137
|
+
output += summaryTable.toString() + "\n";
|
|
138
|
+
output += import_chalk.default.gray((0, import_i18n.t)("output.contributorHint")) + "\n";
|
|
139
|
+
return output;
|
|
140
|
+
}
|
|
104
141
|
for (const stats of sortedAuthors) {
|
|
105
142
|
output += import_chalk.default.bold.cyan(`
|
|
106
143
|
${stats.author}
|
|
@@ -176,7 +213,8 @@ ${stats.author}
|
|
|
176
213
|
}
|
|
177
214
|
return output;
|
|
178
215
|
}
|
|
179
|
-
formatJSON(result) {
|
|
216
|
+
formatJSON(result, showContributors = true) {
|
|
217
|
+
const sortedAuthors = Array.from(result.authors.values());
|
|
180
218
|
const data = {
|
|
181
219
|
timeRange: {
|
|
182
220
|
since: result.timeRange.since.toISOString(),
|
|
@@ -203,8 +241,10 @@ ${stats.author}
|
|
|
203
241
|
totalDeletions: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0),
|
|
204
242
|
totalNetChanges: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0)
|
|
205
243
|
}
|
|
206
|
-
}
|
|
207
|
-
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
if (showContributors) {
|
|
247
|
+
data.authors = sortedAuthors.map((stats) => {
|
|
208
248
|
var _a;
|
|
209
249
|
return {
|
|
210
250
|
author: stats.author,
|
|
@@ -217,11 +257,23 @@ ${stats.author}
|
|
|
217
257
|
netChanges: stats.netChanges,
|
|
218
258
|
lastCommitDate: (_a = stats.lastCommitDate) == null ? void 0 : _a.toISOString()
|
|
219
259
|
};
|
|
220
|
-
})
|
|
221
|
-
}
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
const totalProposalsSet = /* @__PURE__ */ new Set();
|
|
263
|
+
sortedAuthors.forEach((s) => s.openspecProposals.forEach((p) => totalProposalsSet.add(p)));
|
|
264
|
+
data.authorsSummary = {
|
|
265
|
+
totalContributors: sortedAuthors.length,
|
|
266
|
+
totalCommits: sortedAuthors.reduce((sum, s) => sum + s.commits, 0),
|
|
267
|
+
totalProposals: totalProposalsSet.size,
|
|
268
|
+
totalCodeFiles: sortedAuthors.reduce((sum, s) => sum + s.codeFilesChanged, 0),
|
|
269
|
+
totalAdditions: sortedAuthors.reduce((sum, s) => sum + s.additions, 0),
|
|
270
|
+
totalDeletions: sortedAuthors.reduce((sum, s) => sum + s.deletions, 0),
|
|
271
|
+
totalNetChanges: sortedAuthors.reduce((sum, s) => sum + s.netChanges, 0)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
222
274
|
return JSON.stringify(data, null, 2);
|
|
223
275
|
}
|
|
224
|
-
formatCSV(result) {
|
|
276
|
+
formatCSV(result, showContributors = true) {
|
|
225
277
|
var _a;
|
|
226
278
|
const rows = [];
|
|
227
279
|
rows.push(`
|
|
@@ -260,30 +312,51 @@ ${stats.author}
|
|
|
260
312
|
rows.push(`${(0, import_i18n.t)("table.netChanges")},${totalProposalNetChanges}`);
|
|
261
313
|
rows.push(`
|
|
262
314
|
# ${(0, import_i18n.t)("output.authorSummary")}`);
|
|
263
|
-
rows.push(
|
|
264
|
-
`${(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")}`
|
|
265
|
-
);
|
|
266
315
|
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
267
|
-
|
|
268
|
-
|
|
316
|
+
if (showContributors) {
|
|
317
|
+
rows.push(
|
|
318
|
+
`${(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")}`
|
|
319
|
+
);
|
|
320
|
+
for (const stats of sortedAuthors) {
|
|
321
|
+
const proposals = Array.from(stats.openspecProposals).join(";");
|
|
322
|
+
rows.push(
|
|
323
|
+
[
|
|
324
|
+
stats.author,
|
|
325
|
+
stats.statisticsPeriod || "-",
|
|
326
|
+
stats.commits,
|
|
327
|
+
stats.openspecProposals.size,
|
|
328
|
+
`"${proposals}"`,
|
|
329
|
+
stats.codeFilesChanged,
|
|
330
|
+
stats.additions,
|
|
331
|
+
stats.deletions,
|
|
332
|
+
stats.netChanges,
|
|
333
|
+
((_a = stats.lastCommitDate) == null ? void 0 : _a.toISOString()) || ""
|
|
334
|
+
].join(",")
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
rows.push(
|
|
339
|
+
`${(0, import_i18n.t)("table.contributors")},${(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")}`
|
|
340
|
+
);
|
|
341
|
+
const totalProposalsSet = /* @__PURE__ */ new Set();
|
|
342
|
+
sortedAuthors.forEach((s) => s.openspecProposals.forEach((p) => totalProposalsSet.add(p)));
|
|
269
343
|
rows.push(
|
|
270
344
|
[
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
stats.deletions,
|
|
279
|
-
stats.netChanges,
|
|
280
|
-
((_a = stats.lastCommitDate) == null ? void 0 : _a.toISOString()) || ""
|
|
345
|
+
sortedAuthors.length,
|
|
346
|
+
sortedAuthors.reduce((sum, s) => sum + s.commits, 0),
|
|
347
|
+
totalProposalsSet.size,
|
|
348
|
+
sortedAuthors.reduce((sum, s) => sum + s.codeFilesChanged, 0),
|
|
349
|
+
sortedAuthors.reduce((sum, s) => sum + s.additions, 0),
|
|
350
|
+
sortedAuthors.reduce((sum, s) => sum + s.deletions, 0),
|
|
351
|
+
sortedAuthors.reduce((sum, s) => sum + s.netChanges, 0)
|
|
281
352
|
].join(",")
|
|
282
353
|
);
|
|
354
|
+
rows.push("");
|
|
355
|
+
rows.push(`# ${(0, import_i18n.t)("output.contributorHint")}`);
|
|
283
356
|
}
|
|
284
357
|
return rows.join("\n");
|
|
285
358
|
}
|
|
286
|
-
formatMarkdown(result) {
|
|
359
|
+
formatMarkdown(result, showContributors = true) {
|
|
287
360
|
let md = "";
|
|
288
361
|
md += (0, import_i18n.t)("markdown.title");
|
|
289
362
|
md += (0, import_i18n.t)("markdown.timeRange", {
|
|
@@ -330,26 +403,45 @@ ${stats.author}
|
|
|
330
403
|
## ${(0, import_i18n.t)("output.authorSummary")}
|
|
331
404
|
|
|
332
405
|
`;
|
|
333
|
-
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")} |
|
|
334
|
-
`;
|
|
335
|
-
md += "|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n";
|
|
336
406
|
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
337
|
-
|
|
338
|
-
md += `| ${
|
|
407
|
+
if (showContributors) {
|
|
408
|
+
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")} |
|
|
339
409
|
`;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
410
|
+
md += "|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n";
|
|
411
|
+
for (const stats of sortedAuthors) {
|
|
412
|
+
md += `| ${stats.author} | ${stats.statisticsPeriod || "-"} | ${stats.commits} | ${stats.openspecProposals.size} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
|
|
413
|
+
`;
|
|
414
|
+
}
|
|
415
|
+
md += (0, import_i18n.t)("markdown.proposalDetails");
|
|
416
|
+
for (const stats of sortedAuthors) {
|
|
417
|
+
if (stats.openspecProposals.size > 0) {
|
|
418
|
+
md += `### ${stats.author}
|
|
345
419
|
|
|
346
420
|
`;
|
|
347
|
-
|
|
348
|
-
|
|
421
|
+
for (const proposal of Array.from(stats.openspecProposals)) {
|
|
422
|
+
md += `- ${proposal}
|
|
349
423
|
`;
|
|
424
|
+
}
|
|
425
|
+
md += "\n";
|
|
350
426
|
}
|
|
351
|
-
md += "\n";
|
|
352
427
|
}
|
|
428
|
+
} else {
|
|
429
|
+
md += `| ${(0, import_i18n.t)("table.contributors")} | ${(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")} |
|
|
430
|
+
`;
|
|
431
|
+
md += "|------------|---------|-----------|------------|-----------|-----------|-------------|\n";
|
|
432
|
+
const totalProposalsSet = /* @__PURE__ */ new Set();
|
|
433
|
+
sortedAuthors.forEach((s) => s.openspecProposals.forEach((p) => totalProposalsSet.add(p)));
|
|
434
|
+
const totalAuthors = sortedAuthors.length;
|
|
435
|
+
const totalCommits = sortedAuthors.reduce((sum, s) => sum + s.commits, 0);
|
|
436
|
+
const totalFiles = sortedAuthors.reduce((sum, s) => sum + s.codeFilesChanged, 0);
|
|
437
|
+
const totalAdditions = sortedAuthors.reduce((sum, s) => sum + s.additions, 0);
|
|
438
|
+
const totalDeletions = sortedAuthors.reduce((sum, s) => sum + s.deletions, 0);
|
|
439
|
+
const totalNetChanges = sortedAuthors.reduce((sum, s) => sum + s.netChanges, 0);
|
|
440
|
+
md += `| ${totalAuthors} | ${totalCommits} | ${totalProposalsSet.size} | ${totalFiles} | +${totalAdditions} | -${totalDeletions} | ${totalNetChanges >= 0 ? "+" : ""}${totalNetChanges} |
|
|
441
|
+
`;
|
|
442
|
+
md += `
|
|
443
|
+
*${(0, import_i18n.t)("output.contributorHint")}*
|
|
444
|
+
`;
|
|
353
445
|
}
|
|
354
446
|
return md;
|
|
355
447
|
}
|
|
@@ -3,6 +3,7 @@ export declare class GitAnalyzer {
|
|
|
3
3
|
private git;
|
|
4
4
|
private config;
|
|
5
5
|
constructor(repoPath: string, config: Config);
|
|
6
|
+
fetchRemote(): Promise<void>;
|
|
6
7
|
getCommits(since: Date, until: Date, branches: string[]): Promise<CommitInfo[]>;
|
|
7
8
|
getCommitBranches(commitHash: string, targetBranches: string[]): Promise<string[]>;
|
|
8
9
|
analyzeCommit(commit: CommitInfo): Promise<CommitAnalysis | null>;
|
package/dist/cjs/git-analyzer.js
CHANGED
|
@@ -39,6 +39,12 @@ var GitAnalyzer = class {
|
|
|
39
39
|
this.git = (0, import_simple_git.default)(repoPath);
|
|
40
40
|
this.config = config;
|
|
41
41
|
}
|
|
42
|
+
async fetchRemote() {
|
|
43
|
+
try {
|
|
44
|
+
await this.git.fetch();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
42
48
|
async getCommits(since, until, branches) {
|
|
43
49
|
const sinceStr = since.toISOString();
|
|
44
50
|
const untilStr = until.toISOString();
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"loading.config": "🔍 Loading configuration...",
|
|
17
17
|
"loading.activeUsers": "🔍 Fetching active users...",
|
|
18
18
|
"loading.analyzing": "🔍 Analyzing commit history...",
|
|
19
|
+
"loading.fetching": "🔄 Fetching remote branches...",
|
|
19
20
|
|
|
20
21
|
"info.timeRange": "📅 Time Range: {{since}} ~ {{until}}",
|
|
21
22
|
"info.branches": "🌿 Branches: {{branches}}",
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
"output.proposalTotal": " 📊 Total: {{count}} proposals | {{commits}} commits | {{files}} files | +{{additions}}/-{{deletions}} lines (net: {{netChanges}})\n",
|
|
53
54
|
"output.proposalTotalLabel": "Proposal Summary Total",
|
|
54
55
|
"output.authorSummary": "👥 Author Summary (by contributor)",
|
|
56
|
+
"output.contributorHint": "💡 Use --show-contributors to see detailed statistics for each contributor",
|
|
55
57
|
|
|
56
58
|
"table.branch": "Branch",
|
|
57
59
|
"table.period": "Period",
|
|
@@ -74,5 +76,76 @@
|
|
|
74
76
|
"markdown.branches": "**Branches**: {{branches}}\n\n",
|
|
75
77
|
"markdown.totalCommits": "**Total Commits**: {{count}}\n\n",
|
|
76
78
|
"markdown.statistics": "## Statistics\n\n",
|
|
77
|
-
"markdown.proposalDetails": "\n## Proposal Details\n\n"
|
|
79
|
+
"markdown.proposalDetails": "\n## Proposal Details\n\n",
|
|
80
|
+
|
|
81
|
+
"multi.beta.warning": "⚠️ BETA: Multi-repository mode is experimental",
|
|
82
|
+
"multi.beta.feedback": " Please report issues at: https://github.com/Orchardxyz/openspec-stat/issues",
|
|
83
|
+
"multi.loading.config": "🔍 Loading multi-repository configuration...",
|
|
84
|
+
"multi.repo.cloning": "☁️ Cloning {{repo}}...",
|
|
85
|
+
"multi.repo.cloned": "✅ Successfully cloned {{repo}}",
|
|
86
|
+
"multi.repo.fetching": "🔄 Fetching remote branches for {{repo}}...",
|
|
87
|
+
"multi.repo.analyzing": "📊 Analyzing {{repo}} ({{type}})...",
|
|
88
|
+
"multi.repo.completed": "✅ Completed {{repo}}: {{commits}} commits",
|
|
89
|
+
"multi.repo.failed": "❌ Failed {{repo}}: {{error}}",
|
|
90
|
+
"multi.repo.skipped": "⏭️ Skipped {{repo}}: disabled",
|
|
91
|
+
"multi.cleanup.start": "🧹 Cleaning up temporary directories...",
|
|
92
|
+
"multi.cleanup.done": "✅ Cleanup completed",
|
|
93
|
+
"multi.summary.title": "\n📦 Multi-Repository Summary\n",
|
|
94
|
+
"multi.summary.repos": "Repositories: {{total}} ({{success}} succeeded, {{failed}} failed)",
|
|
95
|
+
"multi.progress.batch": "Processing batch {{current}}/{{total}}...",
|
|
96
|
+
"multi.table.repository": "Repository",
|
|
97
|
+
"multi.table.type": "Type",
|
|
98
|
+
|
|
99
|
+
"init.welcome": "\n📋 OpenSpec Configuration Wizard\n",
|
|
100
|
+
"init.welcomeMulti": "\n📋 OpenSpec Multi-Repository Configuration Wizard (BETA)\n",
|
|
101
|
+
"init.configName": "Configuration file name:",
|
|
102
|
+
"init.addRepository": "\n📦 Repository {{number}}",
|
|
103
|
+
"init.repoType": "Repository type:",
|
|
104
|
+
"init.repoType.local": "📁 Local - I have this repository on my machine",
|
|
105
|
+
"init.repoType.remote": "☁️ Remote - Clone from remote URL",
|
|
106
|
+
"init.repoName": "Repository name (for display):",
|
|
107
|
+
"init.repoPath": "Local path (absolute or relative):",
|
|
108
|
+
"init.repoUrl": "Git URL (e.g., git@github.com:org/repo.git):",
|
|
109
|
+
"init.repoUrlInvalid": "Invalid Git URL format",
|
|
110
|
+
"init.useFullClone": "Use full clone (recommended for accuracy)?",
|
|
111
|
+
"init.cloneDepth": "Clone depth (number of commits):",
|
|
112
|
+
"init.branches": "Branches to analyze (comma-separated):",
|
|
113
|
+
"init.addMore": "Add another repository?",
|
|
114
|
+
"init.timeConfig": "\n⏰ Time Range Configuration",
|
|
115
|
+
"init.useDefaultTime": "Use default time range (yesterday 20:00 - today 20:00)?",
|
|
116
|
+
"init.sinceHours": "Start time offset (hours, negative for past):",
|
|
117
|
+
"init.untilHours": "End time (hour of day, 0-23):",
|
|
118
|
+
"init.advanced": "\n⚙️ Advanced Options",
|
|
119
|
+
"init.configureAdvanced": "Configure advanced options?",
|
|
120
|
+
"init.openspecDir": "OpenSpec directory path:",
|
|
121
|
+
"init.maxConcurrent": "Maximum concurrent repository operations:",
|
|
122
|
+
"init.authorMapping": "Configure author name mapping?",
|
|
123
|
+
"init.authorMappingInfo": "\nAuthor mapping helps unify multiple Git identities for the same person.\n",
|
|
124
|
+
"init.gitIdentity": "Git identity (name or email):",
|
|
125
|
+
"init.unifiedName": "Unified name for \"{{identity}}\":",
|
|
126
|
+
"init.addMoreMapping": "Add another mapping?",
|
|
127
|
+
"init.preview": "\n✅ Configuration Preview:\n",
|
|
128
|
+
"init.save": "\nSave this configuration?",
|
|
129
|
+
"init.saved": "\n✅ Configuration saved to {{path}}",
|
|
130
|
+
"init.runCommand": "\nRun: openspec-stat multi -c {{path}}",
|
|
131
|
+
"init.templateCreated": "✅ Template created at {{path}}",
|
|
132
|
+
"init.templateEdit": "Please edit the file and configure your repositories.",
|
|
133
|
+
|
|
134
|
+
"config.validation.noRepos": "Invalid config: \"repositories\" must be an array",
|
|
135
|
+
"config.validation.emptyRepos": "Invalid config: at least one repository is required",
|
|
136
|
+
"config.validation.noName": "Repository {{index}}: \"name\" is required",
|
|
137
|
+
"config.validation.invalidType": "Repository \"{{name}}\": \"type\" must be \"local\" or \"remote\"",
|
|
138
|
+
"config.validation.noPath": "Repository \"{{name}}\": \"path\" is required for local type",
|
|
139
|
+
"config.validation.noUrl": "Repository \"{{name}}\": \"url\" is required for remote type",
|
|
140
|
+
"config.validation.noBranches": "Repository \"{{name}}\": at least one branch is required",
|
|
141
|
+
"config.summary.title": "\n📋 Configuration Summary\n",
|
|
142
|
+
"config.summary.repositories": "Repositories:",
|
|
143
|
+
"config.summary.timeRange": "\nTime Range:",
|
|
144
|
+
"config.summary.since": " Since: {{hours}} hours",
|
|
145
|
+
"config.summary.until": " Until: {{hours}}:00",
|
|
146
|
+
"config.summary.parallelism": "\nParallelism:",
|
|
147
|
+
"config.summary.maxConcurrent": " Max concurrent: {{count}}",
|
|
148
|
+
"config.summary.remoteCache": "\nRemote Cache:",
|
|
149
|
+
"config.summary.cacheDir": " Directory: {{dir}}",
|
|
150
|
+
"config.summary.autoCleanup": " Auto cleanup: {{enabled}}"
|
|
78
151
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"loading.config": "🔍 正在加载配置...",
|
|
17
17
|
"loading.activeUsers": "🔍 正在获取活跃用户...",
|
|
18
18
|
"loading.analyzing": "🔍 正在分析提交历史...",
|
|
19
|
+
"loading.fetching": "🔄 正在拉取远程分支...",
|
|
19
20
|
|
|
20
21
|
"info.timeRange": "📅 时间范围:{{since}} ~ {{until}}",
|
|
21
22
|
"info.branches": "🌿 分支:{{branches}}",
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
"output.proposalTotal": " 📊 总计:{{count}} 个提案 | {{commits}} 次提交 | {{files}} 个文件 | +{{additions}}/-{{deletions}} 行(净变更:{{netChanges}})\n",
|
|
53
54
|
"output.proposalTotalLabel": "提案汇总总计",
|
|
54
55
|
"output.authorSummary": "👥 作者汇总(按贡献者统计)",
|
|
56
|
+
"output.contributorHint": "💡 使用 --show-contributors 选项可查看每个贡献者的详细统计信息",
|
|
55
57
|
|
|
56
58
|
"table.branch": "分支",
|
|
57
59
|
"table.period": "周期",
|
|
@@ -74,5 +76,76 @@
|
|
|
74
76
|
"markdown.branches": "**分支**:{{branches}}\n\n",
|
|
75
77
|
"markdown.totalCommits": "**总提交数**:{{count}}\n\n",
|
|
76
78
|
"markdown.statistics": "## 统计数据\n\n",
|
|
77
|
-
"markdown.proposalDetails": "\n## 提案详情\n\n"
|
|
79
|
+
"markdown.proposalDetails": "\n## 提案详情\n\n",
|
|
80
|
+
|
|
81
|
+
"multi.beta.warning": "⚠️ 测试版:多仓库模式为实验性功能",
|
|
82
|
+
"multi.beta.feedback": " 请反馈问题至:https://github.com/Orchardxyz/openspec-stat/issues",
|
|
83
|
+
"multi.loading.config": "🔍 正在加载多仓库配置...",
|
|
84
|
+
"multi.repo.cloning": "☁️ 正在克隆 {{repo}}...",
|
|
85
|
+
"multi.repo.cloned": "✅ 成功克隆 {{repo}}",
|
|
86
|
+
"multi.repo.fetching": "🔄 正在拉取 {{repo}} 的远程分支...",
|
|
87
|
+
"multi.repo.analyzing": "📊 正在分析 {{repo}} ({{type}})...",
|
|
88
|
+
"multi.repo.completed": "✅ 完成 {{repo}}:{{commits}} 次提交",
|
|
89
|
+
"multi.repo.failed": "❌ 失败 {{repo}}:{{error}}",
|
|
90
|
+
"multi.repo.skipped": "⏭️ 跳过 {{repo}}:已禁用",
|
|
91
|
+
"multi.cleanup.start": "🧹 正在清理临时目录...",
|
|
92
|
+
"multi.cleanup.done": "✅ 清理完成",
|
|
93
|
+
"multi.summary.title": "\n📦 多仓库汇总\n",
|
|
94
|
+
"multi.summary.repos": "仓库:{{total}} 个({{success}} 成功,{{failed}} 失败)",
|
|
95
|
+
"multi.progress.batch": "正在处理批次 {{current}}/{{total}}...",
|
|
96
|
+
"multi.table.repository": "仓库",
|
|
97
|
+
"multi.table.type": "类型",
|
|
98
|
+
|
|
99
|
+
"init.welcome": "\n📋 OpenSpec 配置向导\n",
|
|
100
|
+
"init.welcomeMulti": "\n📋 OpenSpec 多仓库配置向导(测试版)\n",
|
|
101
|
+
"init.configName": "配置文件名:",
|
|
102
|
+
"init.addRepository": "\n📦 仓库 {{number}}",
|
|
103
|
+
"init.repoType": "仓库类型:",
|
|
104
|
+
"init.repoType.local": "📁 本地 - 我的机器上已有此仓库",
|
|
105
|
+
"init.repoType.remote": "☁️ 远程 - 从远程 URL 克隆",
|
|
106
|
+
"init.repoName": "仓库名称(用于显示):",
|
|
107
|
+
"init.repoPath": "本地路径(绝对或相对路径):",
|
|
108
|
+
"init.repoUrl": "Git URL(例如:git@github.com:org/repo.git):",
|
|
109
|
+
"init.repoUrlInvalid": "无效的 Git URL 格式",
|
|
110
|
+
"init.useFullClone": "使用完整克隆(推荐以确保准确性)?",
|
|
111
|
+
"init.cloneDepth": "克隆深度(提交数量):",
|
|
112
|
+
"init.branches": "要分析的分支(逗号分隔):",
|
|
113
|
+
"init.addMore": "添加另一个仓库?",
|
|
114
|
+
"init.timeConfig": "\n⏰ 时间范围配置",
|
|
115
|
+
"init.useDefaultTime": "使用默认时间范围(昨天 20:00 - 今天 20:00)?",
|
|
116
|
+
"init.sinceHours": "开始时间偏移(小时,负数表示过去):",
|
|
117
|
+
"init.untilHours": "结束时间(一天中的小时,0-23):",
|
|
118
|
+
"init.advanced": "\n⚙️ 高级选项",
|
|
119
|
+
"init.configureAdvanced": "配置高级选项?",
|
|
120
|
+
"init.openspecDir": "OpenSpec 目录路径:",
|
|
121
|
+
"init.maxConcurrent": "最大并发仓库操作数:",
|
|
122
|
+
"init.authorMapping": "配置作者名称映射?",
|
|
123
|
+
"init.authorMappingInfo": "\n作者映射帮助统一同一人的多个 Git 身份。\n",
|
|
124
|
+
"init.gitIdentity": "Git 身份(名称或邮箱):",
|
|
125
|
+
"init.unifiedName": "\"{{identity}}\" 的统一名称:",
|
|
126
|
+
"init.addMoreMapping": "添加另一个映射?",
|
|
127
|
+
"init.preview": "\n✅ 配置预览:\n",
|
|
128
|
+
"init.save": "\n保存此配置?",
|
|
129
|
+
"init.saved": "\n✅ 配置已保存至 {{path}}",
|
|
130
|
+
"init.runCommand": "\n运行:openspec-stat multi -c {{path}}",
|
|
131
|
+
"init.templateCreated": "✅ 模板已创建于 {{path}}",
|
|
132
|
+
"init.templateEdit": "请编辑文件并配置您的仓库。",
|
|
133
|
+
|
|
134
|
+
"config.validation.noRepos": "无效配置:\"repositories\" 必须是数组",
|
|
135
|
+
"config.validation.emptyRepos": "无效配置:至少需要一个仓库",
|
|
136
|
+
"config.validation.noName": "仓库 {{index}}:需要 \"name\"",
|
|
137
|
+
"config.validation.invalidType": "仓库 \"{{name}}\":\"type\" 必须是 \"local\" 或 \"remote\"",
|
|
138
|
+
"config.validation.noPath": "仓库 \"{{name}}\":本地类型需要 \"path\"",
|
|
139
|
+
"config.validation.noUrl": "仓库 \"{{name}}\":远程类型需要 \"url\"",
|
|
140
|
+
"config.validation.noBranches": "仓库 \"{{name}}\":至少需要一个分支",
|
|
141
|
+
"config.summary.title": "\n📋 配置摘要\n",
|
|
142
|
+
"config.summary.repositories": "仓库:",
|
|
143
|
+
"config.summary.timeRange": "\n时间范围:",
|
|
144
|
+
"config.summary.since": " 开始:{{hours}} 小时",
|
|
145
|
+
"config.summary.until": " 结束:{{hours}}:00",
|
|
146
|
+
"config.summary.parallelism": "\n并发:",
|
|
147
|
+
"config.summary.maxConcurrent": " 最大并发数:{{count}}",
|
|
148
|
+
"config.summary.remoteCache": "\n远程缓存:",
|
|
149
|
+
"config.summary.cacheDir": " 目录:{{dir}}",
|
|
150
|
+
"config.summary.autoCleanup": " 自动清理:{{enabled}}"
|
|
78
151
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/multi/config-validator.ts
|
|
30
|
+
var config_validator_exports = {};
|
|
31
|
+
__export(config_validator_exports, {
|
|
32
|
+
printConfigSummary: () => printConfigSummary,
|
|
33
|
+
validateAndFillDefaults: () => validateAndFillDefaults
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(config_validator_exports);
|
|
36
|
+
var import_chalk = __toESM(require("chalk"));
|
|
37
|
+
var import_i18n = require("../i18n/index.js");
|
|
38
|
+
var DEFAULT_MULTI_REPO_CONFIG = {
|
|
39
|
+
mode: "multi-repo",
|
|
40
|
+
defaultSinceHours: -30,
|
|
41
|
+
defaultUntilHours: 20,
|
|
42
|
+
openspecDir: "openspec/",
|
|
43
|
+
excludeExtensions: [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp"],
|
|
44
|
+
activeUserWeeks: 2,
|
|
45
|
+
authorMapping: {},
|
|
46
|
+
parallelism: {
|
|
47
|
+
maxConcurrent: 3,
|
|
48
|
+
timeout: 6e5
|
|
49
|
+
},
|
|
50
|
+
remoteCache: {
|
|
51
|
+
dir: "/tmp/openspec-stat-cache",
|
|
52
|
+
autoCleanup: true,
|
|
53
|
+
cleanupOnComplete: true,
|
|
54
|
+
cleanupOnError: true
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function validateAndFillDefaults(config) {
|
|
58
|
+
if (!config.repositories || !Array.isArray(config.repositories)) {
|
|
59
|
+
throw new Error((0, import_i18n.t)("config.validation.noRepos"));
|
|
60
|
+
}
|
|
61
|
+
if (config.repositories.length === 0) {
|
|
62
|
+
throw new Error((0, import_i18n.t)("config.validation.emptyRepos"));
|
|
63
|
+
}
|
|
64
|
+
config.repositories.forEach((repo, index) => {
|
|
65
|
+
if (!repo.name) {
|
|
66
|
+
throw new Error((0, import_i18n.t)("config.validation.noName", { index: String(index + 1) }));
|
|
67
|
+
}
|
|
68
|
+
if (!repo.type || !["local", "remote"].includes(repo.type)) {
|
|
69
|
+
throw new Error((0, import_i18n.t)("config.validation.invalidType", { name: repo.name }));
|
|
70
|
+
}
|
|
71
|
+
if (repo.type === "local" && !repo.path) {
|
|
72
|
+
throw new Error((0, import_i18n.t)("config.validation.noPath", { name: repo.name }));
|
|
73
|
+
}
|
|
74
|
+
if (repo.type === "remote" && !repo.url) {
|
|
75
|
+
throw new Error((0, import_i18n.t)("config.validation.noUrl", { name: repo.name }));
|
|
76
|
+
}
|
|
77
|
+
if (!repo.branches || !Array.isArray(repo.branches) || repo.branches.length === 0) {
|
|
78
|
+
throw new Error((0, import_i18n.t)("config.validation.noBranches", { name: repo.name }));
|
|
79
|
+
}
|
|
80
|
+
if (repo.enabled === void 0) {
|
|
81
|
+
repo.enabled = true;
|
|
82
|
+
}
|
|
83
|
+
if (repo.type === "remote" && !repo.cloneOptions) {
|
|
84
|
+
repo.cloneOptions = { depth: null, singleBranch: false };
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const mergedConfig = {
|
|
88
|
+
...DEFAULT_MULTI_REPO_CONFIG,
|
|
89
|
+
...config,
|
|
90
|
+
parallelism: {
|
|
91
|
+
...DEFAULT_MULTI_REPO_CONFIG.parallelism,
|
|
92
|
+
...config.parallelism
|
|
93
|
+
},
|
|
94
|
+
remoteCache: {
|
|
95
|
+
...DEFAULT_MULTI_REPO_CONFIG.remoteCache,
|
|
96
|
+
...config.remoteCache
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return mergedConfig;
|
|
100
|
+
}
|
|
101
|
+
function printConfigSummary(config) {
|
|
102
|
+
var _a, _b, _c, _d;
|
|
103
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("config.summary.title")));
|
|
104
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("config.summary.repositories")));
|
|
105
|
+
(_a = config.repositories) == null ? void 0 : _a.forEach((repo, i) => {
|
|
106
|
+
const icon = repo.type === "local" ? "📁" : "☁️";
|
|
107
|
+
const location = repo.type === "local" ? repo.path : repo.url;
|
|
108
|
+
console.log(` ${i + 1}. ${icon} ${import_chalk.default.bold(repo.name)} (${repo.type})`);
|
|
109
|
+
console.log(` ${import_chalk.default.gray(location)}`);
|
|
110
|
+
console.log(` ${import_chalk.default.gray("Branches:")} ${repo.branches.join(", ")}`);
|
|
111
|
+
});
|
|
112
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("config.summary.timeRange")));
|
|
113
|
+
console.log((0, import_i18n.t)("config.summary.since", { hours: String(config.defaultSinceHours) }));
|
|
114
|
+
console.log((0, import_i18n.t)("config.summary.until", { hours: String(config.defaultUntilHours) }));
|
|
115
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("config.summary.parallelism")));
|
|
116
|
+
console.log((0, import_i18n.t)("config.summary.maxConcurrent", { count: String(((_b = config.parallelism) == null ? void 0 : _b.maxConcurrent) || 3) }));
|
|
117
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("config.summary.remoteCache")));
|
|
118
|
+
console.log((0, import_i18n.t)("config.summary.cacheDir", { dir: ((_c = config.remoteCache) == null ? void 0 : _c.dir) || "/tmp/openspec-stat-cache" }));
|
|
119
|
+
console.log(
|
|
120
|
+
(0, import_i18n.t)("config.summary.autoCleanup", {
|
|
121
|
+
enabled: ((_d = config.remoteCache) == null ? void 0 : _d.cleanupOnComplete) ? "Yes" : "No"
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
console.log();
|
|
125
|
+
}
|
|
126
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
127
|
+
0 && (module.exports = {
|
|
128
|
+
printConfigSummary,
|
|
129
|
+
validateAndFillDefaults
|
|
130
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare function runConfigWizard(isMultiRepo?: boolean): Promise<void>;
|
|
2
|
+
export declare const SINGLE_REPO_TEMPLATE: {
|
|
3
|
+
defaultBranches: string[];
|
|
4
|
+
defaultSinceHours: number;
|
|
5
|
+
defaultUntilHours: number;
|
|
6
|
+
authorMapping: {
|
|
7
|
+
'user@email1.com': string;
|
|
8
|
+
'user@email2.com': string;
|
|
9
|
+
};
|
|
10
|
+
openspecDir: string;
|
|
11
|
+
excludeExtensions: string[];
|
|
12
|
+
activeUserWeeks: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const MULTI_REPO_TEMPLATE: {
|
|
15
|
+
mode: string;
|
|
16
|
+
repositories: ({
|
|
17
|
+
name: string;
|
|
18
|
+
type: string;
|
|
19
|
+
path: string;
|
|
20
|
+
branches: string[];
|
|
21
|
+
url?: undefined;
|
|
22
|
+
cloneOptions?: undefined;
|
|
23
|
+
} | {
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
url: string;
|
|
27
|
+
branches: string[];
|
|
28
|
+
cloneOptions: {
|
|
29
|
+
depth: null;
|
|
30
|
+
singleBranch: boolean;
|
|
31
|
+
};
|
|
32
|
+
path?: undefined;
|
|
33
|
+
})[];
|
|
34
|
+
defaultSinceHours: number;
|
|
35
|
+
defaultUntilHours: number;
|
|
36
|
+
authorMapping: {};
|
|
37
|
+
openspecDir: string;
|
|
38
|
+
excludeExtensions: string[];
|
|
39
|
+
activeUserWeeks: number;
|
|
40
|
+
parallelism: {
|
|
41
|
+
maxConcurrent: number;
|
|
42
|
+
timeout: number;
|
|
43
|
+
};
|
|
44
|
+
remoteCache: {
|
|
45
|
+
dir: string;
|
|
46
|
+
autoCleanup: boolean;
|
|
47
|
+
cleanupOnComplete: boolean;
|
|
48
|
+
cleanupOnError: boolean;
|
|
49
|
+
};
|
|
50
|
+
};
|