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
package/README.md CHANGED
@@ -11,12 +11,14 @@ A CLI tool for tracking team members' OpenSpec proposals and code changes in Git
11
11
 
12
12
  - ✅ Track Git commits within specified time ranges
13
13
  - ✅ Identify commits containing both OpenSpec proposals and code changes
14
+ - ✅ **Proposal-based statistics summary** - Aggregate statistics by proposal to avoid merge commit bias
14
15
  - ✅ Group statistics by author (commits, proposals, code changes)
15
16
  - ✅ Support multiple branches and wildcard filtering
16
17
  - ✅ Author name mapping (handle multiple Git accounts for the same person)
17
18
  - ✅ Track only recently active members (default: 2 weeks)
18
19
  - ✅ Multiple output formats: Table, JSON, CSV, Markdown
19
20
  - ✅ Internationalization support: English and Chinese (简体中文)
21
+ - ✅ **🆕 Multi-repository mode (BETA)** - Analyze multiple local/remote repositories in one run
20
22
 
21
23
  ## Installation
22
24
 
@@ -48,6 +50,33 @@ openspec-stat
48
50
 
49
51
  This will track commits in the default time range (yesterday 20:00 ~ today 20:00).
50
52
 
53
+ ### Multi-Repository Mode (BETA)
54
+
55
+ ⚠️ **Experimental Feature**: Multi-repository mode allows analyzing multiple repositories (local or remote) in a single run.
56
+
57
+ ```bash
58
+ # Initialize multi-repo configuration (interactive wizard)
59
+ openspec-stat init --multi
60
+
61
+ # Run multi-repo analysis
62
+ openspec-stat multi -c .openspec-stats.multi.json
63
+
64
+ # Run with detailed contributor statistics
65
+ openspec-stat multi -c .openspec-stats.multi.json --show-contributors
66
+
67
+ # Generate template
68
+ openspec-stat init --template multi -o config.json
69
+ ```
70
+
71
+ **Perfect for team managers who:**
72
+ - Have local access to backend repos but not frontend repos
73
+ - Need to track contributions across multiple repositories
74
+ - Want combined statistics without running multiple commands
75
+
76
+ **Note**: By default, multi-repo mode only shows aggregated statistics to avoid information overload. Use `--show-contributors` to see detailed statistics for each contributor.
77
+
78
+ See [Multi-Repository Guide](./MULTI_REPO_GUIDE.md) for detailed documentation.
79
+
51
80
  ### Command Line Options
52
81
 
53
82
  ```bash
@@ -163,6 +192,11 @@ Statistics include:
163
192
  - **Deletions**: Lines of code deleted
164
193
  - **Net Changes**: Additions - Deletions
165
194
 
195
+ The tool provides two perspectives:
196
+
197
+ 1. **Proposal Summary**: Aggregates statistics by proposal, showing total code changes per proposal and all contributors. This avoids statistical bias from merge commits.
198
+ 2. **Author Summary**: Groups statistics by contributor, showing individual author contributions.
199
+
166
200
  ## Output Formats
167
201
 
168
202
  ### Table Format (Default)
@@ -173,6 +207,16 @@ Time Range: 2024-01-01 00:00:00 ~ 2024-01-31 23:59:59
173
207
  Branches: origin/master
174
208
  Total Commits: 15
175
209
 
210
+ 📋 Proposal Summary (by proposal)
211
+ ┌──────────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
212
+ │ Proposal │ Commits │ Contributors │ Code Files │ Additions │ Deletions │ Net Changes │
213
+ ├──────────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
214
+ │ feature-123 │ 5 │ John Doe, Jane S.│ 30 │ +890 │ -234 │ +656 │
215
+ │ feature-456 │ 3 │ John Doe │ 15 │ +344 │ -100 │ +244 │
216
+ └──────────────┴─────────┴──────────────────┴────────────┴───────────┴───────────┴─────────────┘
217
+ 📊 Total: 2 proposals | 8 commits | 45 files | +1234/-334 lines (net: +900)
218
+
219
+ 👥 Author Summary (by contributor)
176
220
  ┌──────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
177
221
  │ Author │ Commits │ OpenSpec Proposals│ Code Files │ Additions │ Deletions │ Net Changes │
178
222
  ├──────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
package/README.zh-CN.md CHANGED
@@ -11,12 +11,14 @@
11
11
 
12
12
  - ✅ 追踪指定时间范围内的 Git 提交
13
13
  - ✅ 识别同时包含 OpenSpec 提案和代码变更的提交
14
+ - ✅ **提案维度统计汇总** - 按提案聚合统计,避免 merge commit 导致的统计偏差
14
15
  - ✅ 按作者分组统计(提交数、提案数、代码变更)
15
16
  - ✅ 支持多分支和通配符过滤
16
17
  - ✅ 作者名称映射(处理同一人的多个 Git 账号)
17
18
  - ✅ 仅追踪最近活跃的成员(默认:2 周)
18
19
  - ✅ 多种输出格式:表格、JSON、CSV、Markdown
19
20
  - ✅ 国际化支持:英文和中文
21
+ - ✅ **🆕 多仓库模式(BETA)** - 一次运行分析多个本地/远程仓库
20
22
 
21
23
  ## 安装
22
24
 
@@ -48,6 +50,33 @@ openspec-stat
48
50
 
49
51
  这将追踪默认时间范围内的提交(昨天 20:00 ~ 今天 20:00)。
50
52
 
53
+ ### 多仓库模式(BETA)
54
+
55
+ ⚠️ **实验性功能**:多仓库模式允许在单次运行中分析多个仓库(本地或远程)。
56
+
57
+ ```bash
58
+ # 初始化多仓库配置(交互式向导)
59
+ openspec-stat init --multi
60
+
61
+ # 运行多仓库分析
62
+ openspec-stat multi -c .openspec-stats.multi.json
63
+
64
+ # 运行并显示详细的贡献者统计
65
+ openspec-stat multi -c .openspec-stats.multi.json --show-contributors
66
+
67
+ # 生成配置模板
68
+ openspec-stat init --template multi -o config.json
69
+ ```
70
+
71
+ **适用于以下场景的团队管理者:**
72
+ - 拥有后端仓库的本地访问权限,但没有前端仓库
73
+ - 需要跨多个仓库追踪贡献情况
74
+ - 希望获得合并统计结果,而无需运行多个命令
75
+
76
+ **注意**:默认情况下,多仓库模式仅显示聚合统计信息,以避免信息过载。使用 `--show-contributors` 可查看每个贡献者的详细统计信息。
77
+
78
+ 详细文档请参阅 [多仓库模式指南](./MULTI_REPO_GUIDE.md)。
79
+
51
80
  ### 命令行选项
52
81
 
53
82
  ```bash
@@ -163,6 +192,11 @@ openspec-stat --lang zh-CN --verbose
163
192
  - **删除行数**:删除的代码行数
164
193
  - **净变更**:新增行数 - 删除行数
165
194
 
195
+ 工具提供两个统计视角:
196
+
197
+ 1. **提案汇总**:按提案聚合统计,显示每个提案的总代码变更量和所有贡献者,避免 merge commit 导致的统计偏差
198
+ 2. **作者汇总**:按贡献者分组统计,显示各个作者的个人贡献情况
199
+
166
200
  ## 输出格式
167
201
 
168
202
  ### 表格格式(默认)
@@ -173,6 +207,16 @@ openspec-stat --lang zh-CN --verbose
173
207
  分支:origin/master
174
208
  总提交数:15
175
209
 
210
+ 📋 提案汇总(按提案统计)
211
+ ┌──────────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
212
+ │ 提案 │ 提交数 │ 贡献者 │ 代码文件 │ 新增行数 │ 删除行数 │ 净变更 │
213
+ ├──────────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
214
+ │ feature-123 │ 5 │ 张三, 李四 │ 30 │ +890 │ -234 │ +656 │
215
+ │ feature-456 │ 3 │ 张三 │ 15 │ +344 │ -100 │ +244 │
216
+ └──────────────┴─────────┴──────────────────┴────────────┴───────────┴───────────┴─────────────┘
217
+ 📊 总计:2 个提案 | 8 次提交 | 45 个文件 | +1234/-334 行(净变更:+900)
218
+
219
+ 👥 作者汇总(按贡献者统计)
176
220
  ┌──────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
177
221
  │ 作者 │ 提交数 │ 提案数 │ 代码文件 │ 新增行数 │ 删除行数 │ 净变更 │
178
222
  ├──────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
package/dist/cjs/cli.js CHANGED
@@ -1,134 +1,19 @@
1
1
  #!/usr/bin/env node
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") {
10
- for (let key of __getOwnPropNames(from))
11
- if (!__hasOwnProp.call(to, key) && key !== except)
12
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
- }
14
- return to;
15
- };
16
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
- // If the importer is in node compatibility mode or this is not an ESM
18
- // file that has been converted to a CommonJS file using a Babel-
19
- // compatible transform (i.e. "__esModule" has not been set), then set
20
- // "default" to the CommonJS "module.exports" for node compatibility.
21
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
- mod
23
- ));
24
2
 
25
3
  // src/cli.ts
26
4
  var import_commander = require("commander");
27
- var import_chalk = __toESM(require("chalk"));
28
- var import_config = require("./config.js");
29
- var import_git_analyzer = require("./git-analyzer.js");
30
- var import_stats_aggregator = require("./stats-aggregator.js");
31
- var import_formatters = require("./formatters.js");
32
- var import_time_utils = require("./time-utils.js");
33
- var import_branch_selector = require("./branch-selector.js");
34
- var import_i18n = require("./i18n/index.js");
5
+ var import_single = require("./commands/single.js");
6
+ var import_multi = require("./commands/multi.js");
7
+ var import_init = require("./commands/init.js");
35
8
  var program = new import_commander.Command();
36
- program.name("openspec-stat").description("Track team members' OpenSpec proposals and code changes in Git repositories").version("0.0.1").option("-r, --repo <path>", "Repository path", ".").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").action(async (options) => {
37
- try {
38
- (0, import_i18n.initI18n)(options.lang);
39
- console.log(import_chalk.default.blue((0, import_i18n.t)("loading.config")));
40
- const config = await (0, import_config.loadConfig)(options.config, options.repo);
41
- let since;
42
- let until;
43
- if (options.since || options.until) {
44
- since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).since;
45
- until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).until;
46
- } else {
47
- const defaultRange = (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours);
48
- since = defaultRange.since;
49
- until = defaultRange.until;
50
- }
51
- let branches;
52
- if (options.branches) {
53
- branches = (0, import_time_utils.parseBranches)(options.branches);
54
- } else if (options.interactive !== false) {
55
- branches = await (0, import_branch_selector.selectBranches)(options.repo, config.defaultBranches);
56
- } else {
57
- branches = config.defaultBranches || [];
58
- }
59
- console.log(
60
- import_chalk.default.blue(
61
- (0, import_i18n.t)("info.timeRange", {
62
- since: since.toLocaleString(),
63
- until: until.toLocaleString()
64
- })
65
- )
66
- );
67
- console.log(
68
- import_chalk.default.blue(
69
- (0, import_i18n.t)("info.branches", {
70
- branches: branches.join(", ") || (0, import_i18n.t)("info.allBranches")
71
- })
72
- )
73
- );
74
- const analyzer = new import_git_analyzer.GitAnalyzer(options.repo, config);
75
- console.log(import_chalk.default.blue((0, import_i18n.t)("loading.activeUsers")));
76
- const activeAuthors = await analyzer.getActiveAuthors(config.activeUserWeeks || 2);
77
- if (options.verbose) {
78
- console.log(
79
- import_chalk.default.gray(
80
- (0, import_i18n.t)("info.activeUsers", {
81
- weeks: String(config.activeUserWeeks || 2),
82
- users: Array.from(activeAuthors).join(", ")
83
- })
84
- )
85
- );
86
- }
87
- console.log(import_chalk.default.blue((0, import_i18n.t)("loading.analyzing")));
88
- const commits = await analyzer.getCommits(since, until, branches);
89
- if (commits.length === 0) {
90
- console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noCommits")));
91
- return;
92
- }
93
- console.log(import_chalk.default.blue((0, import_i18n.t)("info.foundCommits", { count: String(commits.length) })));
94
- const analyses = [];
95
- for (let i = 0; i < commits.length; i++) {
96
- const commit = commits[i];
97
- if (options.verbose && i % 10 === 0) {
98
- console.log(
99
- import_chalk.default.gray(
100
- (0, import_i18n.t)("info.analysisProgress", {
101
- current: String(i + 1),
102
- total: String(commits.length)
103
- })
104
- )
105
- );
106
- }
107
- const analysis = await analyzer.analyzeCommit(commit);
108
- if (analysis) {
109
- analyses.push(analysis);
110
- }
111
- }
112
- if (analyses.length === 0) {
113
- console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits")));
114
- return;
115
- }
116
- console.log(import_chalk.default.blue((0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })));
117
- const aggregator = new import_stats_aggregator.StatsAggregator(config, activeAuthors);
118
- const result = aggregator.aggregate(analyses, since, until, branches, options.author);
119
- const formatter = new import_formatters.OutputFormatter();
120
- if (options.json) {
121
- console.log(formatter.formatJSON(result));
122
- } else if (options.csv) {
123
- console.log(formatter.formatCSV(result));
124
- } else if (options.markdown) {
125
- console.log(formatter.formatMarkdown(result));
126
- } else {
127
- console.log(formatter.formatTable(result, options.verbose));
128
- }
129
- } catch (error) {
130
- console.error(import_chalk.default.red((0, import_i18n.t)("error.prefix")), error);
131
- process.exit(1);
132
- }
9
+ program.name("openspec-stat").description("Track team members' OpenSpec proposals and code changes in Git repositories").version("0.0.1");
10
+ program.option("-r, --repo <path>", "Repository path", ".").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 (options) => {
11
+ await (0, import_single.runSingleRepoCommand)(options);
12
+ });
13
+ program.command("multi").description("Multi-repository analysis mode (BETA)").option("-c, --config <path>", "Configuration file path", ".openspec-stats.multi.json").option("-s, --since <datetime>", "Override start time").option("-u, --until <datetime>", "Override end time").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("-v, --verbose", "Verbose output mode").option("-l, --lang <language>", "Language (en, zh-CN)", "en").option("--no-cleanup", "Do not cleanup temporary directories").option("--show-contributors", "Show detailed contributor statistics (default: only show summary)").option("--no-fetch", "Skip fetching remote branches for local repositories").action(async (options) => {
14
+ await (0, import_multi.runMultiRepoCommand)(options);
15
+ });
16
+ program.command("init").description("Initialize configuration file").option("--multi", "Create multi-repository configuration (interactive)").option("--template <type>", "Generate template (single|multi)").option("-o, --output <path>", "Output file path").action(async (options) => {
17
+ await (0, import_init.runInitCommand)(options);
133
18
  });
134
19
  program.parse();
@@ -0,0 +1,7 @@
1
+ interface InitOptions {
2
+ multi?: boolean;
3
+ template?: 'single' | 'multi';
4
+ output?: string;
5
+ }
6
+ export declare function runInitCommand(options: InitOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,58 @@
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/init.ts
30
+ var init_exports = {};
31
+ __export(init_exports, {
32
+ runInitCommand: () => runInitCommand
33
+ });
34
+ module.exports = __toCommonJS(init_exports);
35
+ var import_chalk = __toESM(require("chalk"));
36
+ var import_fs = require("fs");
37
+ var import_i18n = require("../i18n/index.js");
38
+ var import_config_wizard = require("../multi/config-wizard.js");
39
+ async function runInitCommand(options) {
40
+ try {
41
+ if (options.template) {
42
+ const template = options.template === "multi" ? import_config_wizard.MULTI_REPO_TEMPLATE : import_config_wizard.SINGLE_REPO_TEMPLATE;
43
+ const outputPath = options.output || `.openspec-stats.${options.template}.json`;
44
+ (0, import_fs.writeFileSync)(outputPath, JSON.stringify(template, null, 2));
45
+ console.log(import_chalk.default.green((0, import_i18n.t)("init.templateCreated", { path: outputPath })));
46
+ console.log(import_chalk.default.gray((0, import_i18n.t)("init.templateEdit")));
47
+ return;
48
+ }
49
+ await (0, import_config_wizard.runConfigWizard)(options.multi || false);
50
+ } catch (error) {
51
+ console.error(import_chalk.default.red((0, import_i18n.t)("error.prefix")), error);
52
+ process.exit(1);
53
+ }
54
+ }
55
+ // Annotate the CommonJS export names for ESM import in node:
56
+ 0 && (module.exports = {
57
+ runInitCommand
58
+ });
@@ -0,0 +1,16 @@
1
+ interface MultiCommandOptions {
2
+ config: string;
3
+ since?: string;
4
+ until?: string;
5
+ author?: string;
6
+ json?: boolean;
7
+ csv?: boolean;
8
+ markdown?: boolean;
9
+ verbose?: boolean;
10
+ lang?: string;
11
+ cleanup?: boolean;
12
+ showContributors?: boolean;
13
+ noFetch?: boolean;
14
+ }
15
+ export declare function runMultiRepoCommand(options: MultiCommandOptions): Promise<void>;
16
+ export {};
@@ -0,0 +1,172 @@
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/multi.ts
30
+ var multi_exports = {};
31
+ __export(multi_exports, {
32
+ runMultiRepoCommand: () => runMultiRepoCommand
33
+ });
34
+ module.exports = __toCommonJS(multi_exports);
35
+ var import_chalk = __toESM(require("chalk"));
36
+ var import_fs = require("fs");
37
+ var import_path = require("path");
38
+ var import_i18n = require("../i18n/index.js");
39
+ var import_multi_repo_analyzer = require("../multi/multi-repo-analyzer.js");
40
+ var import_config_validator = require("../multi/config-validator.js");
41
+ var import_stats_aggregator = require("../stats-aggregator.js");
42
+ var import_formatters = require("../formatters.js");
43
+ var import_time_utils = require("../time-utils.js");
44
+ async function runMultiRepoCommand(options) {
45
+ try {
46
+ (0, import_i18n.initI18n)(options.lang || "en");
47
+ console.log(import_chalk.default.yellow.bold((0, import_i18n.t)("multi.beta.warning")));
48
+ console.log(import_chalk.default.gray((0, import_i18n.t)("multi.beta.feedback")));
49
+ console.log();
50
+ console.log(import_chalk.default.blue((0, import_i18n.t)("multi.loading.config")));
51
+ const configPath = (0, import_path.resolve)(process.cwd(), options.config);
52
+ if (!(0, import_fs.existsSync)(configPath)) {
53
+ throw new Error(`Configuration file not found: ${configPath}`);
54
+ }
55
+ const rawConfig = JSON.parse((0, import_fs.readFileSync)(configPath, "utf-8"));
56
+ const config = (0, import_config_validator.validateAndFillDefaults)(rawConfig);
57
+ if (options.verbose) {
58
+ (0, import_config_validator.printConfigSummary)(config);
59
+ }
60
+ let since;
61
+ let until;
62
+ if (options.since || options.until) {
63
+ since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).since;
64
+ until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).until;
65
+ } else {
66
+ const defaultRange = (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours);
67
+ since = defaultRange.since;
68
+ until = defaultRange.until;
69
+ }
70
+ console.log(
71
+ import_chalk.default.blue(
72
+ (0, import_i18n.t)("info.timeRange", {
73
+ since: since.toLocaleString(),
74
+ until: until.toLocaleString()
75
+ })
76
+ )
77
+ );
78
+ const analyzer = new import_multi_repo_analyzer.MultiRepoAnalyzer(config);
79
+ analyzer.registerCleanupHandlers();
80
+ if (options.cleanup === false) {
81
+ config.remoteCache = config.remoteCache || {
82
+ dir: "/tmp/openspec-stat-cache",
83
+ autoCleanup: false,
84
+ cleanupOnComplete: false,
85
+ cleanupOnError: false
86
+ };
87
+ config.remoteCache.cleanupOnComplete = false;
88
+ }
89
+ if (options.noFetch) {
90
+ config.autoFetch = false;
91
+ }
92
+ const repoResults = await analyzer.analyzeAll(since, until);
93
+ const successResults = repoResults.filter((r) => r.success);
94
+ const failedResults = repoResults.filter((r) => !r.success);
95
+ console.log(
96
+ import_chalk.default.blue(
97
+ (0, import_i18n.t)("multi.summary.title") + (0, import_i18n.t)("multi.summary.repos", {
98
+ total: String(repoResults.length),
99
+ success: String(successResults.length),
100
+ failed: String(failedResults.length)
101
+ })
102
+ )
103
+ );
104
+ if (failedResults.length > 0) {
105
+ console.log(import_chalk.default.yellow("\nFailed repositories:"));
106
+ failedResults.forEach((r) => {
107
+ console.log(import_chalk.default.red(` - ${r.repository}: ${r.error}`));
108
+ });
109
+ console.log();
110
+ }
111
+ const allAnalyses = successResults.flatMap((r) => r.analyses);
112
+ if (allAnalyses.length === 0) {
113
+ console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits")));
114
+ return;
115
+ }
116
+ console.log(import_chalk.default.blue((0, import_i18n.t)("info.qualifyingCommits", { count: String(allAnalyses.length) })));
117
+ console.log(import_chalk.default.blue((0, import_i18n.t)("loading.activeUsers")));
118
+ const activeAuthors = await getActiveAuthorsFromMultiRepo(config, repoResults);
119
+ if (options.verbose && activeAuthors.size > 0) {
120
+ console.log(
121
+ import_chalk.default.gray(
122
+ (0, import_i18n.t)("info.activeUsers", {
123
+ weeks: String(config.activeUserWeeks || 2),
124
+ users: Array.from(activeAuthors).join(", ")
125
+ })
126
+ )
127
+ );
128
+ }
129
+ const aggregator = new import_stats_aggregator.StatsAggregator(config, activeAuthors);
130
+ const allBranches = [
131
+ ...new Set(
132
+ repoResults.flatMap((r) => {
133
+ var _a, _b;
134
+ return ((_b = (_a = config.repositories) == null ? void 0 : _a.find((repo) => repo.name === r.repository)) == null ? void 0 : _b.branches) || [];
135
+ })
136
+ )
137
+ ];
138
+ const result = aggregator.aggregate(allAnalyses, since, until, allBranches, options.author);
139
+ const formatter = new import_formatters.OutputFormatter();
140
+ const showContributors = options.showContributors || false;
141
+ if (options.json) {
142
+ console.log(formatter.formatJSON(result, showContributors));
143
+ } else if (options.csv) {
144
+ console.log(formatter.formatCSV(result, showContributors));
145
+ } else if (options.markdown) {
146
+ console.log(formatter.formatMarkdown(result, showContributors));
147
+ } else {
148
+ console.log(formatter.formatTable(result, options.verbose, showContributors));
149
+ }
150
+ } catch (error) {
151
+ console.error(import_chalk.default.red((0, import_i18n.t)("error.prefix")), error);
152
+ process.exit(1);
153
+ }
154
+ }
155
+ async function getActiveAuthorsFromMultiRepo(config, repoResults) {
156
+ const allAuthors = /* @__PURE__ */ new Set();
157
+ for (const result of repoResults) {
158
+ if (result.success && result.analyses) {
159
+ result.analyses.forEach((analysis) => {
160
+ var _a;
161
+ if ((_a = analysis.commit) == null ? void 0 : _a.author) {
162
+ allAuthors.add(analysis.commit.author);
163
+ }
164
+ });
165
+ }
166
+ }
167
+ return allAuthors;
168
+ }
169
+ // Annotate the CommonJS export names for ESM import in node:
170
+ 0 && (module.exports = {
171
+ runMultiRepoCommand
172
+ });
@@ -0,0 +1,2 @@
1
+ import { CliOptions } from '../types.js';
2
+ export declare function runSingleRepoCommand(options: CliOptions): Promise<void>;