openspec-stat 1.1.0 โ†’ 1.3.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.
Files changed (45) hide show
  1. package/README.md +44 -0
  2. package/README.zh-CN.md +44 -0
  3. package/dist/cjs/cli.js +12 -127
  4. package/dist/cjs/commands/init.d.ts +7 -0
  5. package/dist/cjs/commands/init.js +58 -0
  6. package/dist/cjs/commands/multi.d.ts +16 -0
  7. package/dist/cjs/commands/multi.js +172 -0
  8. package/dist/cjs/commands/single.d.ts +2 -0
  9. package/dist/cjs/commands/single.js +148 -0
  10. package/dist/cjs/formatters.d.ts +4 -4
  11. package/dist/cjs/formatters.js +268 -30
  12. package/dist/cjs/git-analyzer.d.ts +1 -0
  13. package/dist/cjs/git-analyzer.js +6 -0
  14. package/dist/cjs/i18n/locales/en.json +80 -1
  15. package/dist/cjs/i18n/locales/zh-CN.json +80 -1
  16. package/dist/cjs/multi/config-validator.d.ts +3 -0
  17. package/dist/cjs/multi/config-validator.js +130 -0
  18. package/dist/cjs/multi/config-wizard.d.ts +50 -0
  19. package/dist/cjs/multi/config-wizard.js +331 -0
  20. package/dist/cjs/multi/multi-repo-analyzer.d.ts +14 -0
  21. package/dist/cjs/multi/multi-repo-analyzer.js +210 -0
  22. package/dist/cjs/stats-aggregator.js +25 -0
  23. package/dist/cjs/types.d.ts +57 -0
  24. package/dist/esm/cli.js +43 -137
  25. package/dist/esm/commands/init.d.ts +7 -0
  26. package/dist/esm/commands/init.js +49 -0
  27. package/dist/esm/commands/multi.d.ts +16 -0
  28. package/dist/esm/commands/multi.js +192 -0
  29. package/dist/esm/commands/single.d.ts +2 -0
  30. package/dist/esm/commands/single.js +162 -0
  31. package/dist/esm/formatters.d.ts +4 -4
  32. package/dist/esm/formatters.js +361 -55
  33. package/dist/esm/git-analyzer.d.ts +1 -0
  34. package/dist/esm/git-analyzer.js +104 -77
  35. package/dist/esm/i18n/locales/en.json +80 -1
  36. package/dist/esm/i18n/locales/zh-CN.json +80 -1
  37. package/dist/esm/multi/config-validator.d.ts +3 -0
  38. package/dist/esm/multi/config-validator.js +109 -0
  39. package/dist/esm/multi/config-wizard.d.ts +50 -0
  40. package/dist/esm/multi/config-wizard.js +535 -0
  41. package/dist/esm/multi/multi-repo-analyzer.d.ts +14 -0
  42. package/dist/esm/multi/multi-repo-analyzer.js +446 -0
  43. package/dist/esm/stats-aggregator.js +29 -0
  44. package/dist/esm/types.d.ts +57 -0
  45. package/package.json +1 -1
@@ -0,0 +1,148 @@
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/commands/single.ts
30
+ var single_exports = {};
31
+ __export(single_exports, {
32
+ runSingleRepoCommand: () => runSingleRepoCommand
33
+ });
34
+ module.exports = __toCommonJS(single_exports);
35
+ var import_chalk = __toESM(require("chalk"));
36
+ var import_config = require("../config.js");
37
+ var import_git_analyzer = require("../git-analyzer.js");
38
+ var import_stats_aggregator = require("../stats-aggregator.js");
39
+ var import_formatters = require("../formatters.js");
40
+ var import_time_utils = require("../time-utils.js");
41
+ var import_branch_selector = require("../branch-selector.js");
42
+ var import_i18n = require("../i18n/index.js");
43
+ async function runSingleRepoCommand(options) {
44
+ try {
45
+ (0, import_i18n.initI18n)(options.lang);
46
+ console.log(import_chalk.default.blue((0, import_i18n.t)("loading.config")));
47
+ const config = await (0, import_config.loadConfig)(options.config, options.repo);
48
+ let since;
49
+ let until;
50
+ if (options.since || options.until) {
51
+ since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).since;
52
+ until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).until;
53
+ } else {
54
+ const defaultRange = (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours);
55
+ since = defaultRange.since;
56
+ until = defaultRange.until;
57
+ }
58
+ let branches;
59
+ if (options.branches) {
60
+ branches = (0, import_time_utils.parseBranches)(options.branches);
61
+ } else if (options.interactive !== false) {
62
+ branches = await (0, import_branch_selector.selectBranches)(options.repo, config.defaultBranches);
63
+ } else {
64
+ branches = config.defaultBranches || [];
65
+ }
66
+ console.log(
67
+ import_chalk.default.blue(
68
+ (0, import_i18n.t)("info.timeRange", {
69
+ since: since.toLocaleString(),
70
+ until: until.toLocaleString()
71
+ })
72
+ )
73
+ );
74
+ console.log(
75
+ import_chalk.default.blue(
76
+ (0, import_i18n.t)("info.branches", {
77
+ branches: branches.join(", ") || (0, import_i18n.t)("info.allBranches")
78
+ })
79
+ )
80
+ );
81
+ const analyzer = new import_git_analyzer.GitAnalyzer(options.repo, config);
82
+ if (!options.noFetch && config.autoFetch !== false) {
83
+ console.log(import_chalk.default.blue((0, import_i18n.t)("loading.fetching")));
84
+ await analyzer.fetchRemote();
85
+ }
86
+ console.log(import_chalk.default.blue((0, import_i18n.t)("loading.activeUsers")));
87
+ const activeAuthors = await analyzer.getActiveAuthors(config.activeUserWeeks || 2);
88
+ if (options.verbose) {
89
+ console.log(
90
+ import_chalk.default.gray(
91
+ (0, import_i18n.t)("info.activeUsers", {
92
+ weeks: String(config.activeUserWeeks || 2),
93
+ users: Array.from(activeAuthors).join(", ")
94
+ })
95
+ )
96
+ );
97
+ }
98
+ console.log(import_chalk.default.blue((0, import_i18n.t)("loading.analyzing")));
99
+ const commits = await analyzer.getCommits(since, until, branches);
100
+ if (commits.length === 0) {
101
+ console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noCommits")));
102
+ return;
103
+ }
104
+ console.log(import_chalk.default.blue((0, import_i18n.t)("info.foundCommits", { count: String(commits.length) })));
105
+ const analyses = [];
106
+ for (let i = 0; i < commits.length; i++) {
107
+ const commit = commits[i];
108
+ if (options.verbose && i % 10 === 0) {
109
+ console.log(
110
+ import_chalk.default.gray(
111
+ (0, import_i18n.t)("info.analysisProgress", {
112
+ current: String(i + 1),
113
+ total: String(commits.length)
114
+ })
115
+ )
116
+ );
117
+ }
118
+ const analysis = await analyzer.analyzeCommit(commit);
119
+ if (analysis) {
120
+ analyses.push(analysis);
121
+ }
122
+ }
123
+ if (analyses.length === 0) {
124
+ console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits")));
125
+ return;
126
+ }
127
+ console.log(import_chalk.default.blue((0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })));
128
+ const aggregator = new import_stats_aggregator.StatsAggregator(config, activeAuthors);
129
+ const result = aggregator.aggregate(analyses, since, until, branches, options.author);
130
+ const formatter = new import_formatters.OutputFormatter();
131
+ if (options.json) {
132
+ console.log(formatter.formatJSON(result));
133
+ } else if (options.csv) {
134
+ console.log(formatter.formatCSV(result));
135
+ } else if (options.markdown) {
136
+ console.log(formatter.formatMarkdown(result));
137
+ } else {
138
+ console.log(formatter.formatTable(result, options.verbose));
139
+ }
140
+ } catch (error) {
141
+ console.error(import_chalk.default.red((0, import_i18n.t)("error.prefix")), error);
142
+ process.exit(1);
143
+ }
144
+ }
145
+ // Annotate the CommonJS export names for ESM import in node:
146
+ 0 && (module.exports = {
147
+ runSingleRepoCommand
148
+ });
@@ -1,7 +1,7 @@
1
1
  import { StatsResult } from './types.js';
2
2
  export declare class OutputFormatter {
3
- formatTable(result: StatsResult, verbose?: boolean): string;
4
- formatJSON(result: StatsResult): string;
5
- formatCSV(result: StatsResult): string;
6
- formatMarkdown(result: StatsResult): string;
3
+ formatTable(result: StatsResult, verbose?: boolean, showContributors?: boolean): string;
4
+ formatJSON(result: StatsResult, showContributors?: boolean): string;
5
+ formatCSV(result: StatsResult, showContributors?: boolean): string;
6
+ formatMarkdown(result: StatsResult, showContributors?: boolean): string;
7
7
  }
@@ -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(
@@ -47,7 +47,97 @@ 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
+ if (result.proposals.size > 0) {
51
+ output += import_chalk.default.bold.magenta(`
52
+ ${(0, import_i18n.t)("output.proposalSummary")}
53
+ `);
54
+ const proposalTable = new import_cli_table3.default({
55
+ head: [
56
+ import_chalk.default.magenta((0, import_i18n.t)("table.proposal")),
57
+ import_chalk.default.magenta((0, import_i18n.t)("table.commits")),
58
+ import_chalk.default.magenta((0, import_i18n.t)("table.contributors")),
59
+ import_chalk.default.magenta((0, import_i18n.t)("table.codeFiles")),
60
+ import_chalk.default.magenta((0, import_i18n.t)("table.additions")),
61
+ import_chalk.default.magenta((0, import_i18n.t)("table.deletions")),
62
+ import_chalk.default.magenta((0, import_i18n.t)("table.netChanges"))
63
+ ],
64
+ style: {
65
+ head: [],
66
+ border: []
67
+ }
68
+ });
69
+ const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
70
+ for (const proposalStats of sortedProposals) {
71
+ const contributors = Array.from(proposalStats.contributors).join(", ");
72
+ proposalTable.push([
73
+ proposalStats.proposal,
74
+ proposalStats.commits.toString(),
75
+ contributors,
76
+ proposalStats.codeFilesChanged.toString(),
77
+ import_chalk.default.green(`+${proposalStats.additions}`),
78
+ import_chalk.default.red(`-${proposalStats.deletions}`),
79
+ proposalStats.netChanges >= 0 ? import_chalk.default.green(`+${proposalStats.netChanges}`) : import_chalk.default.red(`${proposalStats.netChanges}`)
80
+ ]);
81
+ }
82
+ output += proposalTable.toString() + "\n";
83
+ const totalProposals = result.proposals.size;
84
+ const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
85
+ const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
86
+ const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
87
+ const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
88
+ const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
89
+ output += import_chalk.default.gray(
90
+ (0, import_i18n.t)("output.proposalTotal", {
91
+ count: totalProposals.toString(),
92
+ commits: totalProposalCommits.toString(),
93
+ files: totalProposalFiles.toString(),
94
+ additions: totalProposalAdditions.toString(),
95
+ deletions: totalProposalDeletions.toString(),
96
+ netChanges: totalProposalNetChanges.toString()
97
+ })
98
+ );
99
+ }
100
+ output += import_chalk.default.bold.cyan(`
101
+ ${(0, import_i18n.t)("output.authorSummary")}
102
+ `);
50
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
+ }
51
141
  for (const stats of sortedAuthors) {
52
142
  output += import_chalk.default.bold.cyan(`
53
143
  ${stats.author}
@@ -123,7 +213,8 @@ ${stats.author}
123
213
  }
124
214
  return output;
125
215
  }
126
- formatJSON(result) {
216
+ formatJSON(result, showContributors = true) {
217
+ const sortedAuthors = Array.from(result.authors.values());
127
218
  const data = {
128
219
  timeRange: {
129
220
  since: result.timeRange.since.toISOString(),
@@ -131,7 +222,29 @@ ${stats.author}
131
222
  },
132
223
  branches: result.branches,
133
224
  totalCommits: result.totalCommits,
134
- authors: Array.from(result.authors.values()).map((stats) => {
225
+ proposals: {
226
+ items: Array.from(result.proposals.values()).map((stats) => ({
227
+ proposal: stats.proposal,
228
+ commits: stats.commits,
229
+ contributors: Array.from(stats.contributors),
230
+ contributorCount: stats.contributors.size,
231
+ codeFilesChanged: stats.codeFilesChanged,
232
+ additions: stats.additions,
233
+ deletions: stats.deletions,
234
+ netChanges: stats.netChanges
235
+ })),
236
+ summary: {
237
+ totalProposals: result.proposals.size,
238
+ totalCommits: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0),
239
+ totalCodeFiles: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0),
240
+ totalAdditions: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0),
241
+ totalDeletions: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0),
242
+ totalNetChanges: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0)
243
+ }
244
+ }
245
+ };
246
+ if (showContributors) {
247
+ data.authors = sortedAuthors.map((stats) => {
135
248
  var _a;
136
249
  return {
137
250
  author: stats.author,
@@ -144,37 +257,106 @@ ${stats.author}
144
257
  netChanges: stats.netChanges,
145
258
  lastCommitDate: (_a = stats.lastCommitDate) == null ? void 0 : _a.toISOString()
146
259
  };
147
- })
148
- };
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
+ }
149
274
  return JSON.stringify(data, null, 2);
150
275
  }
151
- formatCSV(result) {
276
+ formatCSV(result, showContributors = true) {
152
277
  var _a;
153
278
  const rows = [];
279
+ rows.push(`
280
+ # ${(0, import_i18n.t)("output.proposalSummary")}`);
154
281
  rows.push(
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")}`
282
+ `${(0, import_i18n.t)("table.proposal")},${(0, import_i18n.t)("table.commits")},${(0, import_i18n.t)("table.contributors")},${(0, import_i18n.t)("table.codeFiles")},${(0, import_i18n.t)("table.additions")},${(0, import_i18n.t)("table.deletions")},${(0, import_i18n.t)("table.netChanges")}`
156
283
  );
157
- const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
158
- for (const stats of sortedAuthors) {
159
- const proposals = Array.from(stats.openspecProposals).join(";");
284
+ const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
285
+ for (const stats of sortedProposals) {
286
+ const contributors = Array.from(stats.contributors).join(";");
160
287
  rows.push(
161
288
  [
162
- stats.author,
163
- stats.statisticsPeriod || "-",
289
+ stats.proposal,
164
290
  stats.commits,
165
- stats.openspecProposals.size,
166
- `"${proposals}"`,
291
+ `"${contributors}"`,
167
292
  stats.codeFilesChanged,
168
293
  stats.additions,
169
294
  stats.deletions,
170
- stats.netChanges,
171
- ((_a = stats.lastCommitDate) == null ? void 0 : _a.toISOString()) || ""
295
+ stats.netChanges
172
296
  ].join(",")
173
297
  );
174
298
  }
299
+ const totalProposals = result.proposals.size;
300
+ const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
301
+ const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
302
+ const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
303
+ const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
304
+ const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
305
+ rows.push("");
306
+ rows.push(`# ${(0, import_i18n.t)("output.proposalTotalLabel")}`);
307
+ rows.push(`${(0, import_i18n.t)("table.proposals")},${totalProposals}`);
308
+ rows.push(`${(0, import_i18n.t)("table.commits")},${totalProposalCommits}`);
309
+ rows.push(`${(0, import_i18n.t)("table.codeFiles")},${totalProposalFiles}`);
310
+ rows.push(`${(0, import_i18n.t)("table.additions")},${totalProposalAdditions}`);
311
+ rows.push(`${(0, import_i18n.t)("table.deletions")},${totalProposalDeletions}`);
312
+ rows.push(`${(0, import_i18n.t)("table.netChanges")},${totalProposalNetChanges}`);
313
+ rows.push(`
314
+ # ${(0, import_i18n.t)("output.authorSummary")}`);
315
+ const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
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)));
343
+ rows.push(
344
+ [
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)
352
+ ].join(",")
353
+ );
354
+ rows.push("");
355
+ rows.push(`# ${(0, import_i18n.t)("output.contributorHint")}`);
356
+ }
175
357
  return rows.join("\n");
176
358
  }
177
- formatMarkdown(result) {
359
+ formatMarkdown(result, showContributors = true) {
178
360
  let md = "";
179
361
  md += (0, import_i18n.t)("markdown.title");
180
362
  md += (0, import_i18n.t)("markdown.timeRange", {
@@ -183,27 +365,83 @@ ${stats.author}
183
365
  });
184
366
  md += (0, import_i18n.t)("markdown.branches", { branches: result.branches.join(", ") });
185
367
  md += (0, import_i18n.t)("markdown.totalCommits", { count: String(result.totalCommits) });
186
- md += (0, import_i18n.t)("markdown.statistics");
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")} |
368
+ md += `
369
+ ## ${(0, import_i18n.t)("output.proposalSummary")}
370
+
188
371
  `;
189
- md += "|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n";
190
- const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
191
- for (const stats of sortedAuthors) {
192
- md += `| ${stats.author} | ${stats.statisticsPeriod || "-"} | ${stats.commits} | ${stats.openspecProposals.size} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
372
+ md += `| ${(0, import_i18n.t)("table.proposal")} | ${(0, import_i18n.t)("table.commits")} | ${(0, import_i18n.t)("table.contributors")} | ${(0, import_i18n.t)("table.codeFiles")} | ${(0, import_i18n.t)("table.additions")} | ${(0, import_i18n.t)("table.deletions")} | ${(0, import_i18n.t)("table.netChanges")} |
373
+ `;
374
+ md += "|--------|---------|-------------|------------|-----------|-----------|-------------|\n";
375
+ const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
376
+ for (const stats of sortedProposals) {
377
+ const contributors = Array.from(stats.contributors).join(", ");
378
+ md += `| ${stats.proposal} | ${stats.commits} | ${contributors} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
193
379
  `;
194
380
  }
195
- md += (0, import_i18n.t)("markdown.proposalDetails");
196
- for (const stats of sortedAuthors) {
197
- if (stats.openspecProposals.size > 0) {
198
- md += `### ${stats.author}
381
+ const totalProposals = result.proposals.size;
382
+ const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
383
+ const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
384
+ const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
385
+ const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
386
+ const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
387
+ md += `
388
+ **${(0, import_i18n.t)("output.proposalTotalLabel")}**
389
+ `;
390
+ md += `- ${(0, import_i18n.t)("table.proposals")}: ${totalProposals}
391
+ `;
392
+ md += `- ${(0, import_i18n.t)("table.commits")}: ${totalProposalCommits}
393
+ `;
394
+ md += `- ${(0, import_i18n.t)("table.codeFiles")}: ${totalProposalFiles}
395
+ `;
396
+ md += `- ${(0, import_i18n.t)("table.additions")}: +${totalProposalAdditions}
397
+ `;
398
+ md += `- ${(0, import_i18n.t)("table.deletions")}: -${totalProposalDeletions}
399
+ `;
400
+ md += `- ${(0, import_i18n.t)("table.netChanges")}: ${totalProposalNetChanges >= 0 ? "+" : ""}${totalProposalNetChanges}
401
+ `;
402
+ md += `
403
+ ## ${(0, import_i18n.t)("output.authorSummary")}
199
404
 
200
405
  `;
201
- for (const proposal of Array.from(stats.openspecProposals)) {
202
- md += `- ${proposal}
406
+ const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
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")} |
203
409
  `;
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}
419
+
420
+ `;
421
+ for (const proposal of Array.from(stats.openspecProposals)) {
422
+ md += `- ${proposal}
423
+ `;
424
+ }
425
+ md += "\n";
204
426
  }
205
- md += "\n";
206
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
+ `;
207
445
  }
208
446
  return md;
209
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>;
@@ -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}}",
@@ -48,11 +49,18 @@
48
49
  "output.branches": "Branches: {{branches}}\n",
49
50
  "output.totalCommits": "Total Commits: {{count}}\n\n",
50
51
  "output.proposals": " Proposals: {{proposals}}\n",
52
+ "output.proposalSummary": "๐Ÿ“‹ Proposal Summary (by proposal)",
53
+ "output.proposalTotal": " ๐Ÿ“Š Total: {{count}} proposals | {{commits}} commits | {{files}} files | +{{additions}}/-{{deletions}} lines (net: {{netChanges}})\n",
54
+ "output.proposalTotalLabel": "Proposal Summary Total",
55
+ "output.authorSummary": "๐Ÿ‘ฅ Author Summary (by contributor)",
56
+ "output.contributorHint": "๐Ÿ’ก Use --show-contributors to see detailed statistics for each contributor",
51
57
 
52
58
  "table.branch": "Branch",
53
59
  "table.period": "Period",
54
60
  "table.commits": "Commits",
55
61
  "table.proposals": "Proposals",
62
+ "table.proposal": "Proposal",
63
+ "table.contributors": "Contributors",
56
64
  "table.codeFiles": "Code Files",
57
65
  "table.additions": "Additions",
58
66
  "table.deletions": "Deletions",
@@ -68,5 +76,76 @@
68
76
  "markdown.branches": "**Branches**: {{branches}}\n\n",
69
77
  "markdown.totalCommits": "**Total Commits**: {{count}}\n\n",
70
78
  "markdown.statistics": "## Statistics\n\n",
71
- "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}}"
72
151
  }