openspec-stat 1.0.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.
- package/README.md +234 -0
- package/README.zh-CN.md +234 -0
- package/dist/cjs/branch-selector.d.ts +7 -0
- package/dist/cjs/branch-selector.js +124 -0
- package/dist/cjs/cli.d.ts +2 -0
- package/dist/cjs/cli.js +151 -0
- package/dist/cjs/config.d.ts +3 -0
- package/dist/cjs/config.js +66 -0
- package/dist/cjs/formatters.d.ts +7 -0
- package/dist/cjs/formatters.js +222 -0
- package/dist/cjs/git-analyzer.d.ts +10 -0
- package/dist/cjs/git-analyzer.js +170 -0
- package/dist/cjs/i18n/index.d.ts +7 -0
- package/dist/cjs/i18n/index.js +84 -0
- package/dist/cjs/i18n/locales/en.json +72 -0
- package/dist/cjs/i18n/locales/zh-CN.json +72 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.js +50 -0
- package/dist/cjs/stats-aggregator.d.ts +7 -0
- package/dist/cjs/stats-aggregator.js +117 -0
- package/dist/cjs/time-utils.d.ts +6 -0
- package/dist/cjs/time-utils.js +55 -0
- package/dist/cjs/types.d.ts +77 -0
- package/dist/cjs/types.js +17 -0
- package/dist/esm/branch-selector.d.ts +7 -0
- package/dist/esm/branch-selector.js +218 -0
- package/dist/esm/cli.d.ts +2 -0
- package/dist/esm/cli.js +157 -0
- package/dist/esm/config.d.ts +3 -0
- package/dist/esm/config.js +78 -0
- package/dist/esm/formatters.d.ts +7 -0
- package/dist/esm/formatters.js +207 -0
- package/dist/esm/git-analyzer.d.ts +10 -0
- package/dist/esm/git-analyzer.js +335 -0
- package/dist/esm/i18n/index.d.ts +7 -0
- package/dist/esm/i18n/index.js +49 -0
- package/dist/esm/i18n/locales/en.json +72 -0
- package/dist/esm/i18n/locales/zh-CN.json +72 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/stats-aggregator.d.ts +7 -0
- package/dist/esm/stats-aggregator.js +149 -0
- package/dist/esm/time-utils.d.ts +6 -0
- package/dist/esm/time-utils.js +31 -0
- package/dist/esm/types.d.ts +77 -0
- package/dist/esm/types.js +1 -0
- package/package.json +57 -0
package/dist/cjs/cli.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
|
|
25
|
+
// src/cli.ts
|
|
26
|
+
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");
|
|
35
|
+
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)(
|
|
45
|
+
config.defaultSinceHours,
|
|
46
|
+
config.defaultUntilHours
|
|
47
|
+
).since;
|
|
48
|
+
until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(
|
|
49
|
+
config.defaultSinceHours,
|
|
50
|
+
config.defaultUntilHours
|
|
51
|
+
).until;
|
|
52
|
+
} else {
|
|
53
|
+
const defaultRange = (0, import_time_utils.getDefaultTimeRange)(
|
|
54
|
+
config.defaultSinceHours,
|
|
55
|
+
config.defaultUntilHours
|
|
56
|
+
);
|
|
57
|
+
since = defaultRange.since;
|
|
58
|
+
until = defaultRange.until;
|
|
59
|
+
}
|
|
60
|
+
let branches;
|
|
61
|
+
if (options.branches) {
|
|
62
|
+
branches = (0, import_time_utils.parseBranches)(options.branches);
|
|
63
|
+
} else if (options.interactive !== false) {
|
|
64
|
+
branches = await (0, import_branch_selector.selectBranches)(options.repo, config.defaultBranches);
|
|
65
|
+
} else {
|
|
66
|
+
branches = config.defaultBranches || [];
|
|
67
|
+
}
|
|
68
|
+
console.log(
|
|
69
|
+
import_chalk.default.blue(
|
|
70
|
+
(0, import_i18n.t)("info.timeRange", {
|
|
71
|
+
since: since.toLocaleString(),
|
|
72
|
+
until: until.toLocaleString()
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("info.branches", {
|
|
77
|
+
branches: branches.join(", ") || (0, import_i18n.t)("info.allBranches")
|
|
78
|
+
})));
|
|
79
|
+
const analyzer = new import_git_analyzer.GitAnalyzer(options.repo, config);
|
|
80
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("loading.activeUsers")));
|
|
81
|
+
const activeAuthors = await analyzer.getActiveAuthors(
|
|
82
|
+
config.activeUserWeeks || 2
|
|
83
|
+
);
|
|
84
|
+
if (options.verbose) {
|
|
85
|
+
console.log(
|
|
86
|
+
import_chalk.default.gray(
|
|
87
|
+
(0, import_i18n.t)("info.activeUsers", {
|
|
88
|
+
weeks: String(config.activeUserWeeks || 2),
|
|
89
|
+
users: Array.from(activeAuthors).join(", ")
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("loading.analyzing")));
|
|
95
|
+
const commits = await analyzer.getCommits(since, until, branches);
|
|
96
|
+
if (commits.length === 0) {
|
|
97
|
+
console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noCommits")));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("info.foundCommits", { count: String(commits.length) })));
|
|
101
|
+
const analyses = [];
|
|
102
|
+
for (let i = 0; i < commits.length; i++) {
|
|
103
|
+
const commit = commits[i];
|
|
104
|
+
if (options.verbose && i % 10 === 0) {
|
|
105
|
+
console.log(
|
|
106
|
+
import_chalk.default.gray((0, import_i18n.t)("info.analysisProgress", {
|
|
107
|
+
current: String(i + 1),
|
|
108
|
+
total: String(commits.length)
|
|
109
|
+
}))
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const analysis = await analyzer.analyzeCommit(commit);
|
|
113
|
+
if (analysis) {
|
|
114
|
+
analyses.push(analysis);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (analyses.length === 0) {
|
|
118
|
+
console.log(
|
|
119
|
+
import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits"))
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.log(
|
|
124
|
+
import_chalk.default.blue(
|
|
125
|
+
(0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
const aggregator = new import_stats_aggregator.StatsAggregator(config, activeAuthors);
|
|
129
|
+
const result = aggregator.aggregate(
|
|
130
|
+
analyses,
|
|
131
|
+
since,
|
|
132
|
+
until,
|
|
133
|
+
branches,
|
|
134
|
+
options.author
|
|
135
|
+
);
|
|
136
|
+
const formatter = new import_formatters.OutputFormatter();
|
|
137
|
+
if (options.json) {
|
|
138
|
+
console.log(formatter.formatJSON(result));
|
|
139
|
+
} else if (options.csv) {
|
|
140
|
+
console.log(formatter.formatCSV(result));
|
|
141
|
+
} else if (options.markdown) {
|
|
142
|
+
console.log(formatter.formatMarkdown(result));
|
|
143
|
+
} else {
|
|
144
|
+
console.log(formatter.formatTable(result, options.verbose));
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(import_chalk.default.red((0, import_i18n.t)("error.prefix")), error);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
program.parse();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/config.ts
|
|
20
|
+
var config_exports = {};
|
|
21
|
+
__export(config_exports, {
|
|
22
|
+
loadConfig: () => loadConfig,
|
|
23
|
+
normalizeAuthor: () => normalizeAuthor
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(config_exports);
|
|
26
|
+
var import_fs = require("fs");
|
|
27
|
+
var import_path = require("path");
|
|
28
|
+
var DEFAULT_CONFIG = {
|
|
29
|
+
defaultBranches: ["origin/master"],
|
|
30
|
+
defaultSinceHours: -30,
|
|
31
|
+
defaultUntilHours: 20,
|
|
32
|
+
authorMapping: {},
|
|
33
|
+
openspecDir: "openspec/",
|
|
34
|
+
excludeExtensions: [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp"],
|
|
35
|
+
activeUserWeeks: 2
|
|
36
|
+
};
|
|
37
|
+
async function loadConfig(configPath, repoPath = ".") {
|
|
38
|
+
let config = { ...DEFAULT_CONFIG };
|
|
39
|
+
const searchPaths = configPath ? [configPath] : [
|
|
40
|
+
(0, import_path.resolve)(repoPath, ".openspec-stats.json"),
|
|
41
|
+
(0, import_path.resolve)(repoPath, "openspec-stats.config.json"),
|
|
42
|
+
(0, import_path.resolve)(process.cwd(), ".openspec-stats.json"),
|
|
43
|
+
(0, import_path.resolve)(process.cwd(), "openspec-stats.config.json")
|
|
44
|
+
];
|
|
45
|
+
for (const path of searchPaths) {
|
|
46
|
+
if ((0, import_fs.existsSync)(path)) {
|
|
47
|
+
try {
|
|
48
|
+
const fileContent = (0, import_fs.readFileSync)(path, "utf-8");
|
|
49
|
+
const userConfig = JSON.parse(fileContent);
|
|
50
|
+
config = { ...config, ...userConfig };
|
|
51
|
+
break;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn(`Failed to load config from ${path}:`, error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
function normalizeAuthor(author, mapping = {}) {
|
|
60
|
+
return mapping[author] || author;
|
|
61
|
+
}
|
|
62
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
63
|
+
0 && (module.exports = {
|
|
64
|
+
loadConfig,
|
|
65
|
+
normalizeAuthor
|
|
66
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { StatsResult } from './types.js';
|
|
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;
|
|
7
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
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/formatters.ts
|
|
30
|
+
var formatters_exports = {};
|
|
31
|
+
__export(formatters_exports, {
|
|
32
|
+
OutputFormatter: () => OutputFormatter
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(formatters_exports);
|
|
35
|
+
var import_cli_table3 = __toESM(require("cli-table3"));
|
|
36
|
+
var import_chalk = __toESM(require("chalk"));
|
|
37
|
+
var import_i18n = require("./i18n/index.js");
|
|
38
|
+
var OutputFormatter = class {
|
|
39
|
+
formatTable(result, verbose = false) {
|
|
40
|
+
let output = "";
|
|
41
|
+
output += import_chalk.default.bold((0, import_i18n.t)("output.title"));
|
|
42
|
+
output += import_chalk.default.gray(
|
|
43
|
+
(0, import_i18n.t)("output.timeRange", {
|
|
44
|
+
since: result.timeRange.since.toLocaleString("zh-CN", { hour12: false }),
|
|
45
|
+
until: result.timeRange.until.toLocaleString("zh-CN", { hour12: false })
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
output += import_chalk.default.gray((0, import_i18n.t)("output.branches", { branches: result.branches.join(", ") }));
|
|
49
|
+
output += import_chalk.default.gray((0, import_i18n.t)("output.totalCommits", { count: String(result.totalCommits) }));
|
|
50
|
+
const sortedAuthors = Array.from(result.authors.values()).sort(
|
|
51
|
+
(a, b) => b.commits - a.commits
|
|
52
|
+
);
|
|
53
|
+
for (const stats of sortedAuthors) {
|
|
54
|
+
output += import_chalk.default.bold.cyan(`
|
|
55
|
+
${stats.author}
|
|
56
|
+
`);
|
|
57
|
+
if (stats.branchStats && stats.branchStats.size > 0) {
|
|
58
|
+
const branchTable = new import_cli_table3.default({
|
|
59
|
+
head: [
|
|
60
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.branch")),
|
|
61
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.commits")),
|
|
62
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.proposals")),
|
|
63
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.codeFiles")),
|
|
64
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.additions")),
|
|
65
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.deletions")),
|
|
66
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.netChanges"))
|
|
67
|
+
],
|
|
68
|
+
style: {
|
|
69
|
+
head: [],
|
|
70
|
+
border: []
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const sortedBranches = Array.from(stats.branchStats.values()).sort(
|
|
74
|
+
(a, b) => b.commits - a.commits
|
|
75
|
+
);
|
|
76
|
+
for (const branchStat of sortedBranches) {
|
|
77
|
+
branchTable.push([
|
|
78
|
+
branchStat.branch,
|
|
79
|
+
branchStat.commits.toString(),
|
|
80
|
+
branchStat.openspecProposals.size.toString(),
|
|
81
|
+
branchStat.codeFilesChanged.toString(),
|
|
82
|
+
import_chalk.default.green(`+${branchStat.additions}`),
|
|
83
|
+
import_chalk.default.red(`-${branchStat.deletions}`),
|
|
84
|
+
branchStat.netChanges >= 0 ? import_chalk.default.green(`+${branchStat.netChanges}`) : import_chalk.default.red(`${branchStat.netChanges}`)
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
branchTable.push([
|
|
88
|
+
import_chalk.default.bold.yellow((0, import_i18n.t)("table.totalDeduplicated")),
|
|
89
|
+
import_chalk.default.bold(stats.commits.toString()),
|
|
90
|
+
import_chalk.default.bold(stats.openspecProposals.size.toString()),
|
|
91
|
+
import_chalk.default.bold(stats.codeFilesChanged.toString()),
|
|
92
|
+
import_chalk.default.bold.green(`+${stats.additions}`),
|
|
93
|
+
import_chalk.default.bold.red(`-${stats.deletions}`),
|
|
94
|
+
stats.netChanges >= 0 ? import_chalk.default.bold.green(`+${stats.netChanges}`) : import_chalk.default.bold.red(`${stats.netChanges}`)
|
|
95
|
+
]);
|
|
96
|
+
output += branchTable.toString() + "\n";
|
|
97
|
+
} else {
|
|
98
|
+
const simpleTable = new import_cli_table3.default({
|
|
99
|
+
head: [
|
|
100
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.period")),
|
|
101
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.commits")),
|
|
102
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.proposals")),
|
|
103
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.codeFiles")),
|
|
104
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.additions")),
|
|
105
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.deletions")),
|
|
106
|
+
import_chalk.default.cyan((0, import_i18n.t)("table.netChanges"))
|
|
107
|
+
],
|
|
108
|
+
style: {
|
|
109
|
+
head: [],
|
|
110
|
+
border: []
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
simpleTable.push([
|
|
114
|
+
stats.statisticsPeriod || "-",
|
|
115
|
+
stats.commits.toString(),
|
|
116
|
+
stats.openspecProposals.size.toString(),
|
|
117
|
+
stats.codeFilesChanged.toString(),
|
|
118
|
+
import_chalk.default.green(`+${stats.additions}`),
|
|
119
|
+
import_chalk.default.red(`-${stats.deletions}`),
|
|
120
|
+
stats.netChanges >= 0 ? import_chalk.default.green(`+${stats.netChanges}`) : import_chalk.default.red(`${stats.netChanges}`)
|
|
121
|
+
]);
|
|
122
|
+
output += simpleTable.toString() + "\n";
|
|
123
|
+
}
|
|
124
|
+
if (verbose && stats.openspecProposals.size > 0) {
|
|
125
|
+
output += import_chalk.default.gray((0, import_i18n.t)("output.proposals", { proposals: Array.from(stats.openspecProposals).join(", ") }));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return output;
|
|
129
|
+
}
|
|
130
|
+
formatJSON(result) {
|
|
131
|
+
const data = {
|
|
132
|
+
timeRange: {
|
|
133
|
+
since: result.timeRange.since.toISOString(),
|
|
134
|
+
until: result.timeRange.until.toISOString()
|
|
135
|
+
},
|
|
136
|
+
branches: result.branches,
|
|
137
|
+
totalCommits: result.totalCommits,
|
|
138
|
+
authors: Array.from(result.authors.values()).map((stats) => {
|
|
139
|
+
var _a;
|
|
140
|
+
return {
|
|
141
|
+
author: stats.author,
|
|
142
|
+
commits: stats.commits,
|
|
143
|
+
openspecProposals: Array.from(stats.openspecProposals),
|
|
144
|
+
proposalCount: stats.openspecProposals.size,
|
|
145
|
+
codeFilesChanged: stats.codeFilesChanged,
|
|
146
|
+
additions: stats.additions,
|
|
147
|
+
deletions: stats.deletions,
|
|
148
|
+
netChanges: stats.netChanges,
|
|
149
|
+
lastCommitDate: (_a = stats.lastCommitDate) == null ? void 0 : _a.toISOString()
|
|
150
|
+
};
|
|
151
|
+
})
|
|
152
|
+
};
|
|
153
|
+
return JSON.stringify(data, null, 2);
|
|
154
|
+
}
|
|
155
|
+
formatCSV(result) {
|
|
156
|
+
var _a;
|
|
157
|
+
const rows = [];
|
|
158
|
+
rows.push(
|
|
159
|
+
`${(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")}`
|
|
160
|
+
);
|
|
161
|
+
const sortedAuthors = Array.from(result.authors.values()).sort(
|
|
162
|
+
(a, b) => b.commits - a.commits
|
|
163
|
+
);
|
|
164
|
+
for (const stats of sortedAuthors) {
|
|
165
|
+
const proposals = Array.from(stats.openspecProposals).join(";");
|
|
166
|
+
rows.push(
|
|
167
|
+
[
|
|
168
|
+
stats.author,
|
|
169
|
+
stats.statisticsPeriod || "-",
|
|
170
|
+
stats.commits,
|
|
171
|
+
stats.openspecProposals.size,
|
|
172
|
+
`"${proposals}"`,
|
|
173
|
+
stats.codeFilesChanged,
|
|
174
|
+
stats.additions,
|
|
175
|
+
stats.deletions,
|
|
176
|
+
stats.netChanges,
|
|
177
|
+
((_a = stats.lastCommitDate) == null ? void 0 : _a.toISOString()) || ""
|
|
178
|
+
].join(",")
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return rows.join("\n");
|
|
182
|
+
}
|
|
183
|
+
formatMarkdown(result) {
|
|
184
|
+
let md = "";
|
|
185
|
+
md += (0, import_i18n.t)("markdown.title");
|
|
186
|
+
md += (0, import_i18n.t)("markdown.timeRange", {
|
|
187
|
+
since: result.timeRange.since.toLocaleString("zh-CN", { hour12: false }),
|
|
188
|
+
until: result.timeRange.until.toLocaleString("zh-CN", { hour12: false })
|
|
189
|
+
});
|
|
190
|
+
md += (0, import_i18n.t)("markdown.branches", { branches: result.branches.join(", ") });
|
|
191
|
+
md += (0, import_i18n.t)("markdown.totalCommits", { count: String(result.totalCommits) });
|
|
192
|
+
md += (0, import_i18n.t)("markdown.statistics");
|
|
193
|
+
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")} |
|
|
194
|
+
`;
|
|
195
|
+
md += "|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n";
|
|
196
|
+
const sortedAuthors = Array.from(result.authors.values()).sort(
|
|
197
|
+
(a, b) => b.commits - a.commits
|
|
198
|
+
);
|
|
199
|
+
for (const stats of sortedAuthors) {
|
|
200
|
+
md += `| ${stats.author} | ${stats.statisticsPeriod || "-"} | ${stats.commits} | ${stats.openspecProposals.size} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
md += (0, import_i18n.t)("markdown.proposalDetails");
|
|
204
|
+
for (const stats of sortedAuthors) {
|
|
205
|
+
if (stats.openspecProposals.size > 0) {
|
|
206
|
+
md += `### ${stats.author}
|
|
207
|
+
|
|
208
|
+
`;
|
|
209
|
+
for (const proposal of Array.from(stats.openspecProposals)) {
|
|
210
|
+
md += `- ${proposal}
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
md += "\n";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return md;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
220
|
+
0 && (module.exports = {
|
|
221
|
+
OutputFormatter
|
|
222
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CommitInfo, CommitAnalysis, Config } from './types.js';
|
|
2
|
+
export declare class GitAnalyzer {
|
|
3
|
+
private git;
|
|
4
|
+
private config;
|
|
5
|
+
constructor(repoPath: string, config: Config);
|
|
6
|
+
getCommits(since: Date, until: Date, branches: string[]): Promise<CommitInfo[]>;
|
|
7
|
+
getCommitBranches(commitHash: string, targetBranches: string[]): Promise<string[]>;
|
|
8
|
+
analyzeCommit(commit: CommitInfo): Promise<CommitAnalysis | null>;
|
|
9
|
+
getActiveAuthors(weeks?: number): Promise<Set<string>>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
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/git-analyzer.ts
|
|
30
|
+
var git_analyzer_exports = {};
|
|
31
|
+
__export(git_analyzer_exports, {
|
|
32
|
+
GitAnalyzer: () => GitAnalyzer
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(git_analyzer_exports);
|
|
35
|
+
var import_simple_git = __toESM(require("simple-git"));
|
|
36
|
+
var import_config = require("./config.js");
|
|
37
|
+
var GitAnalyzer = class {
|
|
38
|
+
constructor(repoPath, config) {
|
|
39
|
+
this.git = (0, import_simple_git.default)(repoPath);
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
async getCommits(since, until, branches) {
|
|
43
|
+
const sinceStr = since.toISOString();
|
|
44
|
+
const untilStr = until.toISOString();
|
|
45
|
+
const logOptions = {
|
|
46
|
+
"--since": sinceStr,
|
|
47
|
+
"--until": untilStr,
|
|
48
|
+
"--all": null
|
|
49
|
+
};
|
|
50
|
+
if (branches.length > 0) {
|
|
51
|
+
for (const branch of branches) {
|
|
52
|
+
logOptions[`--remotes=${branch}`] = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const log = await this.git.log(logOptions);
|
|
56
|
+
const commits = [];
|
|
57
|
+
for (const commit of log.all) {
|
|
58
|
+
const commitBranches = await this.getCommitBranches(commit.hash, branches);
|
|
59
|
+
commits.push({
|
|
60
|
+
hash: commit.hash,
|
|
61
|
+
author: commit.author_name,
|
|
62
|
+
email: commit.author_email,
|
|
63
|
+
date: new Date(commit.date),
|
|
64
|
+
message: commit.message,
|
|
65
|
+
branches: commitBranches
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return commits;
|
|
69
|
+
}
|
|
70
|
+
async getCommitBranches(commitHash, targetBranches) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await this.git.raw(["branch", "-r", "--contains", commitHash]);
|
|
73
|
+
const allBranches = result.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD"));
|
|
74
|
+
if (targetBranches.length === 0) {
|
|
75
|
+
return allBranches;
|
|
76
|
+
}
|
|
77
|
+
return allBranches.filter(
|
|
78
|
+
(branch) => targetBranches.some((target) => branch === target || branch.includes(target))
|
|
79
|
+
);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async analyzeCommit(commit) {
|
|
85
|
+
try {
|
|
86
|
+
const show = await this.git.show([
|
|
87
|
+
"--numstat",
|
|
88
|
+
"--format=",
|
|
89
|
+
commit.hash
|
|
90
|
+
]);
|
|
91
|
+
const lines = show.split("\n").filter((line) => line.trim());
|
|
92
|
+
const fileChanges = [];
|
|
93
|
+
const openspecProposals = /* @__PURE__ */ new Set();
|
|
94
|
+
let hasCodeChanges = false;
|
|
95
|
+
const openspecDir = this.config.openspecDir || "openspec/";
|
|
96
|
+
const excludeExts = this.config.excludeExtensions || [];
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const parts = line.split(/\s+/);
|
|
99
|
+
if (parts.length < 3)
|
|
100
|
+
continue;
|
|
101
|
+
const [addStr, delStr, ...pathParts] = parts;
|
|
102
|
+
const path = pathParts.join(" ");
|
|
103
|
+
const additions = addStr === "-" ? 0 : parseInt(addStr, 10);
|
|
104
|
+
const deletions = delStr === "-" ? 0 : parseInt(delStr, 10);
|
|
105
|
+
if (path.startsWith(openspecDir)) {
|
|
106
|
+
const proposalMatch = path.match(
|
|
107
|
+
new RegExp(`^${openspecDir}changes/([^/]+)`)
|
|
108
|
+
);
|
|
109
|
+
if (proposalMatch) {
|
|
110
|
+
openspecProposals.add(proposalMatch[1]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const isExcluded = excludeExts.some((ext) => path.endsWith(ext));
|
|
114
|
+
const isInOpenspec = path.startsWith(openspecDir);
|
|
115
|
+
if (!isExcluded && !isInOpenspec) {
|
|
116
|
+
hasCodeChanges = true;
|
|
117
|
+
fileChanges.push({
|
|
118
|
+
path,
|
|
119
|
+
additions,
|
|
120
|
+
deletions,
|
|
121
|
+
status: "M"
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (openspecProposals.size > 0 && hasCodeChanges) {
|
|
126
|
+
const totalAdditions = fileChanges.reduce(
|
|
127
|
+
(sum, f) => sum + f.additions,
|
|
128
|
+
0
|
|
129
|
+
);
|
|
130
|
+
const totalDeletions = fileChanges.reduce(
|
|
131
|
+
(sum, f) => sum + f.deletions,
|
|
132
|
+
0
|
|
133
|
+
);
|
|
134
|
+
return {
|
|
135
|
+
commit,
|
|
136
|
+
openspecProposals,
|
|
137
|
+
codeFiles: fileChanges,
|
|
138
|
+
totalAdditions,
|
|
139
|
+
totalDeletions,
|
|
140
|
+
netChanges: totalAdditions - totalDeletions
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`Error analyzing commit ${commit.hash}:`, error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async getActiveAuthors(weeks = 2) {
|
|
150
|
+
const since = /* @__PURE__ */ new Date();
|
|
151
|
+
since.setDate(since.getDate() - weeks * 7);
|
|
152
|
+
const log = await this.git.log({
|
|
153
|
+
"--since": since.toISOString(),
|
|
154
|
+
"--all": null
|
|
155
|
+
});
|
|
156
|
+
const authors = /* @__PURE__ */ new Set();
|
|
157
|
+
for (const commit of log.all) {
|
|
158
|
+
const normalizedAuthor = (0, import_config.normalizeAuthor)(
|
|
159
|
+
commit.author_name,
|
|
160
|
+
this.config.authorMapping
|
|
161
|
+
);
|
|
162
|
+
authors.add(normalizedAuthor);
|
|
163
|
+
}
|
|
164
|
+
return authors;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
168
|
+
0 && (module.exports = {
|
|
169
|
+
GitAnalyzer
|
|
170
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Language = 'en' | 'zh-CN';
|
|
2
|
+
type TranslationKey = string;
|
|
3
|
+
export declare function setLanguage(lang: Language): void;
|
|
4
|
+
export declare function getLanguage(): Language;
|
|
5
|
+
export declare function t(key: TranslationKey, params?: Record<string, string | number>): string;
|
|
6
|
+
export declare function initI18n(lang?: string): void;
|
|
7
|
+
export {};
|