@herodevs/cli 2.0.0-beta.12 → 2.0.0-beta.13
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 +51 -17
- package/dist/commands/report/committers.d.ts +23 -0
- package/dist/commands/report/committers.js +147 -0
- package/dist/service/committers.svc.d.ts +70 -0
- package/dist/service/committers.svc.js +196 -0
- package/dist/service/error.svc.d.ts +8 -0
- package/dist/service/error.svc.js +28 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -43,11 +43,11 @@ npm install -g @herodevs/cli@beta
|
|
|
43
43
|
HeroDevs CLI is available as a binary installation, without requiring `npm`. To do that, you may either download and run the script manually, or use the following cURL or Wget command:
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
|
-
curl -o- https://raw.githubusercontent.com/herodevs/cli/v2.0.0-beta.
|
|
46
|
+
curl -o- https://raw.githubusercontent.com/herodevs/cli/v2.0.0-beta.13/scripts/install.sh | bash
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
|
-
wget -qO- https://raw.githubusercontent.com/herodevs/cli/v2.0.0-beta.
|
|
50
|
+
wget -qO- https://raw.githubusercontent.com/herodevs/cli/v2.0.0-beta.13/scripts/install.sh | bash
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
## Scanning Behavior
|
|
@@ -72,7 +72,7 @@ $ npm install -g @herodevs/cli@beta
|
|
|
72
72
|
$ hd COMMAND
|
|
73
73
|
running command...
|
|
74
74
|
$ hd (--version)
|
|
75
|
-
@herodevs/cli/2.0.0-beta.
|
|
75
|
+
@herodevs/cli/2.0.0-beta.12 darwin-arm64 node-v24.10.0
|
|
76
76
|
$ hd --help [COMMAND]
|
|
77
77
|
USAGE
|
|
78
78
|
$ hd COMMAND
|
|
@@ -82,6 +82,7 @@ USAGE
|
|
|
82
82
|
## Commands
|
|
83
83
|
<!-- commands -->
|
|
84
84
|
* [`hd help [COMMAND]`](#hd-help-command)
|
|
85
|
+
* [`hd report committers`](#hd-report-committers)
|
|
85
86
|
* [`hd scan eol`](#hd-scan-eol)
|
|
86
87
|
* [`hd update [CHANNEL]`](#hd-update-channel)
|
|
87
88
|
* **NOTE:** Only applies to [binary installation method](#binary-installation). NPM users should use [`npm install`](#global-npm-installation) to update to the latest version.
|
|
@@ -95,7 +96,7 @@ USAGE
|
|
|
95
96
|
$ hd help [COMMAND...] [-n]
|
|
96
97
|
|
|
97
98
|
ARGUMENTS
|
|
98
|
-
COMMAND... Command to show help for.
|
|
99
|
+
[COMMAND...] Command to show help for.
|
|
99
100
|
|
|
100
101
|
FLAGS
|
|
101
102
|
-n, --nested-commands Include all nested commands in the output.
|
|
@@ -104,7 +105,38 @@ DESCRIPTION
|
|
|
104
105
|
Display help for hd.
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.
|
|
108
|
+
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.34/src/commands/help.ts)_
|
|
109
|
+
|
|
110
|
+
## `hd report committers`
|
|
111
|
+
|
|
112
|
+
Generate report of committers to a git repository
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
USAGE
|
|
116
|
+
$ hd report committers [--json] [-m <value>] [-c] [-s]
|
|
117
|
+
|
|
118
|
+
FLAGS
|
|
119
|
+
-c, --csv Output in CSV format
|
|
120
|
+
-m, --months=<value> [default: 12] The number of months of git history to review
|
|
121
|
+
-s, --save Save the committers report as herodevs.committers.<output>
|
|
122
|
+
|
|
123
|
+
GLOBAL FLAGS
|
|
124
|
+
--json Format output as json.
|
|
125
|
+
|
|
126
|
+
DESCRIPTION
|
|
127
|
+
Generate report of committers to a git repository
|
|
128
|
+
|
|
129
|
+
EXAMPLES
|
|
130
|
+
$ hd report committers
|
|
131
|
+
|
|
132
|
+
$ hd report committers --csv -s
|
|
133
|
+
|
|
134
|
+
$ hd report committers --json
|
|
135
|
+
|
|
136
|
+
$ hd report committers --csv
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.12/src/commands/report/committers.ts)_
|
|
108
140
|
|
|
109
141
|
## `hd scan eol`
|
|
110
142
|
|
|
@@ -112,18 +144,20 @@ Scan a given SBOM for EOL data
|
|
|
112
144
|
|
|
113
145
|
```
|
|
114
146
|
USAGE
|
|
115
|
-
$ hd scan eol [--json] [-f <value> | -d <value>] [-s] [-o <value>] [--saveSbom] [--sbomOutput <value>]
|
|
147
|
+
$ hd scan eol [--json] [-f <value> | -d <value>] [-s] [-o <value>] [--saveSbom] [--sbomOutput <value>]
|
|
148
|
+
[--saveTrimmedSbom] [--hideReportUrl] [--version]
|
|
116
149
|
|
|
117
150
|
FLAGS
|
|
118
|
-
-d, --dir=<value>
|
|
119
|
-
-f, --file=<value>
|
|
120
|
-
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
--
|
|
124
|
-
--
|
|
125
|
-
--saveTrimmedSbom
|
|
126
|
-
--
|
|
151
|
+
-d, --dir=<value> [default: <current directory>] The directory to scan in order to create a cyclonedx SBOM
|
|
152
|
+
-f, --file=<value> The file path of an existing SBOM to scan for EOL (supports CycloneDX and SPDX 2.3 formats)
|
|
153
|
+
-o, --output=<value> Save the generated report to a custom path (defaults to herodevs.report.json when not
|
|
154
|
+
provided)
|
|
155
|
+
-s, --save Save the generated report as herodevs.report.json in the scanned directory
|
|
156
|
+
--hideReportUrl Hide the generated web report URL for this scan
|
|
157
|
+
--saveSbom Save the generated SBOM as herodevs.sbom.json in the scanned directory
|
|
158
|
+
--saveTrimmedSbom Save the trimmed SBOM as herodevs.sbom-trimmed.json in the scanned directory
|
|
159
|
+
--sbomOutput=<value> Save the generated SBOM to a custom path (defaults to herodevs.sbom.json when not provided)
|
|
160
|
+
--version Show CLI version.
|
|
127
161
|
|
|
128
162
|
GLOBAL FLAGS
|
|
129
163
|
--json Format output as json.
|
|
@@ -157,7 +191,7 @@ EXAMPLES
|
|
|
157
191
|
$ hd scan eol --json
|
|
158
192
|
```
|
|
159
193
|
|
|
160
|
-
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.
|
|
194
|
+
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.12/src/commands/scan/eol.ts)_
|
|
161
195
|
|
|
162
196
|
## `hd update [CHANNEL]`
|
|
163
197
|
|
|
@@ -197,7 +231,7 @@ EXAMPLES
|
|
|
197
231
|
$ hd update --available
|
|
198
232
|
```
|
|
199
233
|
|
|
200
|
-
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.
|
|
234
|
+
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.13/src/commands/update.ts)_
|
|
201
235
|
<!-- commandsstop -->
|
|
202
236
|
|
|
203
237
|
## CI/CD Usage
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { type ReportData } from '../../service/committers.svc.ts';
|
|
3
|
+
export default class Committers extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
months: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<ReportData | string>;
|
|
13
|
+
/**
|
|
14
|
+
* Generates structured report data
|
|
15
|
+
* @param entries - parsed git log output for commits
|
|
16
|
+
*/
|
|
17
|
+
private generateReportData;
|
|
18
|
+
/**
|
|
19
|
+
* Fetches git commit data with month and author information
|
|
20
|
+
* @param sinceDate - Date range for git log
|
|
21
|
+
*/
|
|
22
|
+
private fetchGitCommitData;
|
|
23
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Command, Flags } from '@oclif/core';
|
|
5
|
+
import { filenamePrefix } from "../../config/constants.js";
|
|
6
|
+
import { calculateOverallStats, formatAsCsv, formatAsText, groupCommitsByMonth, parseGitLogOutput, } from "../../service/committers.svc.js";
|
|
7
|
+
import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
|
|
8
|
+
export default class Committers extends Command {
|
|
9
|
+
static description = 'Generate report of committers to a git repository';
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> <%= command.id %>',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --csv -s',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --csv',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
months: Flags.integer({
|
|
19
|
+
char: 'm',
|
|
20
|
+
description: 'The number of months of git history to review',
|
|
21
|
+
default: 12,
|
|
22
|
+
}),
|
|
23
|
+
csv: Flags.boolean({
|
|
24
|
+
char: 'c',
|
|
25
|
+
description: 'Output in CSV format',
|
|
26
|
+
default: false,
|
|
27
|
+
}),
|
|
28
|
+
save: Flags.boolean({
|
|
29
|
+
char: 's',
|
|
30
|
+
description: `Save the committers report as ${filenamePrefix}.committers.<output>`,
|
|
31
|
+
default: false,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { flags } = await this.parse(Committers);
|
|
36
|
+
const { months, csv, save } = flags;
|
|
37
|
+
const isJson = this.jsonEnabled();
|
|
38
|
+
const sinceDate = `${months} months ago`;
|
|
39
|
+
this.log('Starting committers report with flags: %O', flags);
|
|
40
|
+
try {
|
|
41
|
+
// Generate structured report data
|
|
42
|
+
const entries = this.fetchGitCommitData(sinceDate);
|
|
43
|
+
this.log('Fetched %d commit entries', entries.length);
|
|
44
|
+
const reportData = this.generateReportData(entries);
|
|
45
|
+
// Handle different output scenarios
|
|
46
|
+
if (isJson) {
|
|
47
|
+
// JSON mode
|
|
48
|
+
if (save) {
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.json`), JSON.stringify(reportData, null, 2));
|
|
51
|
+
this.log('Report written to json');
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
this.error(`Failed to save JSON report: ${getErrorMessage(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return reportData;
|
|
58
|
+
}
|
|
59
|
+
const textOutput = formatAsText(reportData);
|
|
60
|
+
if (csv) {
|
|
61
|
+
// CSV mode
|
|
62
|
+
const csvOutput = formatAsCsv(reportData);
|
|
63
|
+
if (save) {
|
|
64
|
+
try {
|
|
65
|
+
fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.csv`), csvOutput);
|
|
66
|
+
this.log('Report written to csv');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
this.error(`Failed to save CSV report: ${getErrorMessage(error)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.log(textOutput);
|
|
74
|
+
}
|
|
75
|
+
return csvOutput;
|
|
76
|
+
}
|
|
77
|
+
if (save) {
|
|
78
|
+
try {
|
|
79
|
+
fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.txt`), textOutput);
|
|
80
|
+
this.log('Report written to txt');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.error(`Failed to save txt report: ${getErrorMessage(error)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.log(textOutput);
|
|
88
|
+
}
|
|
89
|
+
return textOutput;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.error(`Failed to generate report: ${getErrorMessage(error)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Generates structured report data
|
|
97
|
+
* @param entries - parsed git log output for commits
|
|
98
|
+
*/
|
|
99
|
+
generateReportData(entries) {
|
|
100
|
+
if (entries.length === 0) {
|
|
101
|
+
return { monthly: {}, overall: { total: 0 } };
|
|
102
|
+
}
|
|
103
|
+
const monthlyData = groupCommitsByMonth(entries);
|
|
104
|
+
const overallStats = calculateOverallStats(entries);
|
|
105
|
+
const grandTotal = entries.length;
|
|
106
|
+
// Format into a structured report data object
|
|
107
|
+
const report = {
|
|
108
|
+
monthly: {},
|
|
109
|
+
overall: { ...overallStats, total: grandTotal },
|
|
110
|
+
};
|
|
111
|
+
// Add monthly totals
|
|
112
|
+
for (const [month, authors] of Object.entries(monthlyData)) {
|
|
113
|
+
const monthTotal = Object.values(authors).reduce((sum, count) => sum + count, 0);
|
|
114
|
+
report.monthly[month] = { ...authors, total: monthTotal };
|
|
115
|
+
}
|
|
116
|
+
return report;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fetches git commit data with month and author information
|
|
120
|
+
* @param sinceDate - Date range for git log
|
|
121
|
+
*/
|
|
122
|
+
fetchGitCommitData(sinceDate) {
|
|
123
|
+
const logProcess = spawnSync('git', [
|
|
124
|
+
'log',
|
|
125
|
+
'--all', // Include committers on all branches in the repo
|
|
126
|
+
'--format="%ad|%an"', // Format: date|author
|
|
127
|
+
'--date=format:%Y-%m', // Format date as YYYY-MM
|
|
128
|
+
`--since="${sinceDate}"`,
|
|
129
|
+
], { encoding: 'utf-8' });
|
|
130
|
+
if (logProcess.error) {
|
|
131
|
+
if (isErrnoException(logProcess.error)) {
|
|
132
|
+
if (logProcess.error.code === 'ENOENT') {
|
|
133
|
+
this.error('Git command not found. Please ensure git is installed and available in your PATH.');
|
|
134
|
+
}
|
|
135
|
+
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
|
|
136
|
+
}
|
|
137
|
+
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
|
|
138
|
+
}
|
|
139
|
+
if (logProcess.status !== 0) {
|
|
140
|
+
this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`);
|
|
141
|
+
}
|
|
142
|
+
if (!logProcess.stdout) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
return parseGitLogOutput(logProcess.stdout);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface CommitEntry {
|
|
2
|
+
month: string;
|
|
3
|
+
author: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AuthorCommitCounts {
|
|
6
|
+
[author: string]: number;
|
|
7
|
+
}
|
|
8
|
+
export interface MonthlyData {
|
|
9
|
+
[month: string]: AuthorCommitCounts;
|
|
10
|
+
}
|
|
11
|
+
export interface ReportData {
|
|
12
|
+
monthly: {
|
|
13
|
+
[month: string]: {
|
|
14
|
+
[author: string]: number;
|
|
15
|
+
total: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
overall: {
|
|
19
|
+
[author: string]: number;
|
|
20
|
+
total: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parses git log output into structured data
|
|
25
|
+
* @param output - Git log command output
|
|
26
|
+
* @returns Parsed commit entries
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseGitLogOutput(output: string): CommitEntry[];
|
|
29
|
+
/**
|
|
30
|
+
* Groups commit data by month
|
|
31
|
+
* @param entries - Commit entries
|
|
32
|
+
* @returns Object with months as keys and author commit counts as values
|
|
33
|
+
*/
|
|
34
|
+
export declare function groupCommitsByMonth(entries: CommitEntry[]): MonthlyData;
|
|
35
|
+
/**
|
|
36
|
+
* Calculates overall commit statistics by author
|
|
37
|
+
* @param entries - Commit entries
|
|
38
|
+
* @returns Object with authors as keys and total commit counts as values
|
|
39
|
+
*/
|
|
40
|
+
export declare function calculateOverallStats(entries: CommitEntry[]): AuthorCommitCounts;
|
|
41
|
+
/**
|
|
42
|
+
* Formats monthly report sections
|
|
43
|
+
* @param monthlyData - Grouped commit data by month
|
|
44
|
+
* @returns Formatted monthly report sections
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatMonthlyReport(monthlyData: MonthlyData): string;
|
|
47
|
+
/**
|
|
48
|
+
* Formats overall statistics section
|
|
49
|
+
* @param overallStats - Overall commit counts by author
|
|
50
|
+
* @param grandTotal - Total number of commits
|
|
51
|
+
* @returns Formatted overall statistics section
|
|
52
|
+
*/
|
|
53
|
+
export declare function formatOverallStats(overallStats: AuthorCommitCounts, grandTotal: number): string;
|
|
54
|
+
/**
|
|
55
|
+
* Formats the report data as CSV
|
|
56
|
+
* @param data - The structured report data
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatAsCsv(data: ReportData): string;
|
|
59
|
+
/**
|
|
60
|
+
* Formats the report data as text
|
|
61
|
+
* @param data - The structured report data
|
|
62
|
+
*/
|
|
63
|
+
export declare function formatAsText(data: ReportData): string;
|
|
64
|
+
/**
|
|
65
|
+
* Format output based on user preference
|
|
66
|
+
* @param output
|
|
67
|
+
* @param reportData
|
|
68
|
+
* @returns
|
|
69
|
+
*/
|
|
70
|
+
export declare function formatOutputBasedOnFlag(output: string, reportData: ReportData): string;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses git log output into structured data
|
|
3
|
+
* @param output - Git log command output
|
|
4
|
+
* @returns Parsed commit entries
|
|
5
|
+
*/
|
|
6
|
+
export function parseGitLogOutput(output) {
|
|
7
|
+
return output
|
|
8
|
+
.split('\n')
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.map((line) => {
|
|
11
|
+
// Remove surrounding double quotes if present (e.g. "March|John Doe" → March|John Doe)
|
|
12
|
+
const [month, author] = line.replace(/^"(.*)"$/, '$1').split('|');
|
|
13
|
+
return { month, author };
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Groups commit data by month
|
|
18
|
+
* @param entries - Commit entries
|
|
19
|
+
* @returns Object with months as keys and author commit counts as values
|
|
20
|
+
*/
|
|
21
|
+
export function groupCommitsByMonth(entries) {
|
|
22
|
+
const result = {};
|
|
23
|
+
// Group commits by month
|
|
24
|
+
const commitsByMonth = entries.reduce((acc, entry) => {
|
|
25
|
+
const monthKey = entry.month;
|
|
26
|
+
if (!acc[monthKey]) {
|
|
27
|
+
acc[monthKey] = [];
|
|
28
|
+
}
|
|
29
|
+
acc[monthKey].push(entry);
|
|
30
|
+
return acc;
|
|
31
|
+
}, {});
|
|
32
|
+
// Process each month
|
|
33
|
+
for (const [month, commits] of Object.entries(commitsByMonth)) {
|
|
34
|
+
if (!commits) {
|
|
35
|
+
result[month] = {};
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// Count commits per author for this month
|
|
39
|
+
const commitsByAuthor = commits.reduce((acc, entry) => {
|
|
40
|
+
const authorKey = entry.author;
|
|
41
|
+
if (!acc[authorKey]) {
|
|
42
|
+
acc[authorKey] = [];
|
|
43
|
+
}
|
|
44
|
+
acc[authorKey].push(entry);
|
|
45
|
+
return acc;
|
|
46
|
+
}, {});
|
|
47
|
+
const authorCounts = {};
|
|
48
|
+
for (const [author, authorCommits] of Object.entries(commitsByAuthor)) {
|
|
49
|
+
authorCounts[author] = authorCommits?.length ?? 0;
|
|
50
|
+
}
|
|
51
|
+
result[month] = authorCounts;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Calculates overall commit statistics by author
|
|
57
|
+
* @param entries - Commit entries
|
|
58
|
+
* @returns Object with authors as keys and total commit counts as values
|
|
59
|
+
*/
|
|
60
|
+
export function calculateOverallStats(entries) {
|
|
61
|
+
const commitsByAuthor = entries.reduce((acc, entry) => {
|
|
62
|
+
const authorKey = entry.author;
|
|
63
|
+
if (!acc[authorKey]) {
|
|
64
|
+
acc[authorKey] = [];
|
|
65
|
+
}
|
|
66
|
+
acc[authorKey].push(entry);
|
|
67
|
+
return acc;
|
|
68
|
+
}, {});
|
|
69
|
+
const result = {};
|
|
70
|
+
// Count commits for each author
|
|
71
|
+
for (const author in commitsByAuthor) {
|
|
72
|
+
result[author] = commitsByAuthor[author]?.length ?? 0;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Formats monthly report sections
|
|
78
|
+
* @param monthlyData - Grouped commit data by month
|
|
79
|
+
* @returns Formatted monthly report sections
|
|
80
|
+
*/
|
|
81
|
+
export function formatMonthlyReport(monthlyData) {
|
|
82
|
+
const sortedMonths = Object.keys(monthlyData).sort();
|
|
83
|
+
let report = '';
|
|
84
|
+
for (const month of sortedMonths) {
|
|
85
|
+
report += `\n## ${month}\n`;
|
|
86
|
+
const authors = Object.entries(monthlyData[month]).sort((a, b) => b[1] - a[1]);
|
|
87
|
+
for (const [author, count] of authors) {
|
|
88
|
+
report += `${count.toString().padStart(6)} ${author}\n`;
|
|
89
|
+
}
|
|
90
|
+
const monthTotal = authors.reduce((sum, [_, count]) => sum + count, 0);
|
|
91
|
+
report += `${monthTotal.toString().padStart(6)} TOTAL\n`;
|
|
92
|
+
}
|
|
93
|
+
return report;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Formats overall statistics section
|
|
97
|
+
* @param overallStats - Overall commit counts by author
|
|
98
|
+
* @param grandTotal - Total number of commits
|
|
99
|
+
* @returns Formatted overall statistics section
|
|
100
|
+
*/
|
|
101
|
+
export function formatOverallStats(overallStats, grandTotal) {
|
|
102
|
+
let report = '\n## Overall Statistics\n';
|
|
103
|
+
const sortedStats = Object.entries(overallStats).sort((a, b) => b[1] - a[1]);
|
|
104
|
+
for (const [author, count] of sortedStats) {
|
|
105
|
+
report += `${count.toString().padStart(6)} ${author}\n`;
|
|
106
|
+
}
|
|
107
|
+
report += `${grandTotal.toString().padStart(6)} GRAND TOTAL\n`;
|
|
108
|
+
return report;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Formats the report data as CSV
|
|
112
|
+
* @param data - The structured report data
|
|
113
|
+
*/
|
|
114
|
+
export function formatAsCsv(data) {
|
|
115
|
+
// First prepare all author names (for columns)
|
|
116
|
+
const allAuthors = new Set();
|
|
117
|
+
// Collect all unique author names
|
|
118
|
+
for (const monthData of Object.values(data.monthly)) {
|
|
119
|
+
for (const author of Object.keys(monthData)) {
|
|
120
|
+
if (author !== 'total')
|
|
121
|
+
allAuthors.add(author);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const authors = Array.from(allAuthors).sort();
|
|
125
|
+
// Create CSV header
|
|
126
|
+
let csv = `Month,${authors.join(',')},Total\n`;
|
|
127
|
+
// Add monthly data rows
|
|
128
|
+
const sortedMonths = Object.keys(data.monthly).sort();
|
|
129
|
+
for (const month of sortedMonths) {
|
|
130
|
+
csv += month;
|
|
131
|
+
// Add data for each author
|
|
132
|
+
for (const author of authors) {
|
|
133
|
+
const count = data.monthly[month][author] || 0;
|
|
134
|
+
csv += `,${count}`;
|
|
135
|
+
}
|
|
136
|
+
// Add monthly total
|
|
137
|
+
csv += `,${`${data.monthly[month].total}\n`}`;
|
|
138
|
+
}
|
|
139
|
+
// Add overall totals row
|
|
140
|
+
csv += 'Overall';
|
|
141
|
+
for (const author of authors) {
|
|
142
|
+
const count = data.overall[author] || 0;
|
|
143
|
+
csv += `,${count}`;
|
|
144
|
+
}
|
|
145
|
+
csv += `,${data.overall.total}\n`;
|
|
146
|
+
return csv;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Formats the report data as text
|
|
150
|
+
* @param data - The structured report data
|
|
151
|
+
*/
|
|
152
|
+
export function formatAsText(data) {
|
|
153
|
+
let report = 'Monthly Commit Report\n';
|
|
154
|
+
// Monthly sections
|
|
155
|
+
const sortedMonths = Object.keys(data.monthly).sort();
|
|
156
|
+
for (const month of sortedMonths) {
|
|
157
|
+
report += `\n## ${month}\n`;
|
|
158
|
+
const authors = Object.entries(data.monthly[month])
|
|
159
|
+
.filter(([author]) => author !== 'total')
|
|
160
|
+
.sort((a, b) => b[1] - a[1]);
|
|
161
|
+
for (const [author, count] of authors) {
|
|
162
|
+
report += `${count.toString().padStart(6)} ${author}\n`;
|
|
163
|
+
}
|
|
164
|
+
report += `${data.monthly[month].total.toString().padStart(6)} TOTAL\n`;
|
|
165
|
+
}
|
|
166
|
+
// Overall statistics
|
|
167
|
+
report += '\n## Overall Statistics\n';
|
|
168
|
+
const sortedEntries = Object.entries(data.overall)
|
|
169
|
+
.filter(([author]) => author !== 'total')
|
|
170
|
+
.sort((a, b) => b[1] - a[1]);
|
|
171
|
+
for (const [author, count] of sortedEntries) {
|
|
172
|
+
report += `${count.toString().padStart(6)} ${author}\n`;
|
|
173
|
+
}
|
|
174
|
+
report += `${data.overall.total.toString().padStart(6)} GRAND TOTAL\n`;
|
|
175
|
+
return report;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Format output based on user preference
|
|
179
|
+
* @param output
|
|
180
|
+
* @param reportData
|
|
181
|
+
* @returns
|
|
182
|
+
*/
|
|
183
|
+
export function formatOutputBasedOnFlag(output, reportData) {
|
|
184
|
+
let formattedOutput;
|
|
185
|
+
switch (output) {
|
|
186
|
+
case 'json':
|
|
187
|
+
formattedOutput = JSON.stringify(reportData, null, 2);
|
|
188
|
+
break;
|
|
189
|
+
case 'csv':
|
|
190
|
+
formattedOutput = formatAsCsv(reportData);
|
|
191
|
+
break;
|
|
192
|
+
default:
|
|
193
|
+
formattedOutput = formatAsText(reportData);
|
|
194
|
+
}
|
|
195
|
+
return formattedOutput;
|
|
196
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const isError: (error: unknown) => error is Error;
|
|
2
|
+
export declare const isErrnoException: (error: unknown) => error is NodeJS.ErrnoException;
|
|
3
|
+
export declare const isApolloError: (error: unknown) => error is ApolloError;
|
|
4
|
+
export declare const getErrorMessage: (error: unknown) => string;
|
|
5
|
+
export declare class ApolloError extends Error {
|
|
6
|
+
readonly originalError?: unknown;
|
|
7
|
+
constructor(message: string, original?: unknown);
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const isError = (error) => {
|
|
2
|
+
return error instanceof Error;
|
|
3
|
+
};
|
|
4
|
+
export const isErrnoException = (error) => {
|
|
5
|
+
return isError(error) && 'code' in error;
|
|
6
|
+
};
|
|
7
|
+
export const isApolloError = (error) => {
|
|
8
|
+
return error instanceof ApolloError;
|
|
9
|
+
};
|
|
10
|
+
export const getErrorMessage = (error) => {
|
|
11
|
+
if (isError(error)) {
|
|
12
|
+
return error.message;
|
|
13
|
+
}
|
|
14
|
+
return 'Unknown error';
|
|
15
|
+
};
|
|
16
|
+
export class ApolloError extends Error {
|
|
17
|
+
originalError;
|
|
18
|
+
constructor(message, original) {
|
|
19
|
+
if (isError(original)) {
|
|
20
|
+
super(`${message}: ${original.message}`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
super(`${message}: ${String(original)}`);
|
|
24
|
+
}
|
|
25
|
+
this.name = 'ApolloError';
|
|
26
|
+
this.originalError = original;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herodevs/cli",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.13",
|
|
4
4
|
"author": "HeroDevs, Inc",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "./bin/run.js"
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"herodevs cli"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@amplitude/analytics-node": "^1.5.
|
|
42
|
+
"@amplitude/analytics-node": "^1.5.21",
|
|
43
43
|
"@apollo/client": "^3.13.8",
|
|
44
44
|
"@cyclonedx/cdxgen": "^11.11.0",
|
|
45
|
-
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.
|
|
46
|
-
"@oclif/core": "^4.
|
|
45
|
+
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.12",
|
|
46
|
+
"@oclif/core": "^4.8.0",
|
|
47
47
|
"@oclif/plugin-help": "^6.2.32",
|
|
48
48
|
"@oclif/plugin-update": "^4.7.13",
|
|
49
49
|
"node-machine-id": "^1.1.12",
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"update-notifier": "^7.3.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@biomejs/biome": "^2.
|
|
56
|
+
"@biomejs/biome": "^2.3.3",
|
|
57
57
|
"@oclif/test": "^4.1.13",
|
|
58
58
|
"@types/inquirer": "^9.0.9",
|
|
59
|
-
"@types/node": "^24.
|
|
59
|
+
"@types/node": "^24.10.0",
|
|
60
60
|
"@types/sinon": "^17.0.4",
|
|
61
61
|
"@types/update-notifier": "^6.0.8",
|
|
62
62
|
"globstar": "^1.0.0",
|
|
@@ -110,4 +110,4 @@
|
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
"types": "dist/index.d.ts"
|
|
113
|
-
}
|
|
113
|
+
}
|