@herodevs/cli 2.0.0-beta.13 → 2.0.0-beta.14
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 +52 -12
- package/dist/api/gql-operations.js +2 -1
- package/dist/api/nes.client.d.ts +2 -2
- package/dist/api/nes.client.js +10 -7
- package/dist/commands/report/committers.d.ts +11 -7
- package/dist/commands/report/committers.js +144 -76
- package/dist/commands/tracker/init.d.ts +14 -0
- package/dist/commands/tracker/init.js +84 -0
- package/dist/config/constants.d.ts +4 -0
- package/dist/config/constants.js +5 -0
- package/dist/config/tracker.config.d.ts +16 -0
- package/dist/config/tracker.config.js +18 -0
- package/dist/service/committers.svc.d.ts +46 -58
- package/dist/service/committers.svc.js +55 -173
- package/dist/service/tracker.svc.d.ts +3 -0
- package/dist/service/tracker.svc.js +26 -0
- package/package.json +12 -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.14/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.14/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.14 darwin-arm64 node-v24.10.0
|
|
76
76
|
$ hd --help [COMMAND]
|
|
77
77
|
USAGE
|
|
78
78
|
$ hd COMMAND
|
|
@@ -84,6 +84,7 @@ USAGE
|
|
|
84
84
|
* [`hd help [COMMAND]`](#hd-help-command)
|
|
85
85
|
* [`hd report committers`](#hd-report-committers)
|
|
86
86
|
* [`hd scan eol`](#hd-scan-eol)
|
|
87
|
+
* [`hd tracker init`](#hd-tracker-init)
|
|
87
88
|
* [`hd update [CHANNEL]`](#hd-update-channel)
|
|
88
89
|
* **NOTE:** Only applies to [binary installation method](#binary-installation). NPM users should use [`npm install`](#global-npm-installation) to update to the latest version.
|
|
89
90
|
|
|
@@ -113,15 +114,20 @@ Generate report of committers to a git repository
|
|
|
113
114
|
|
|
114
115
|
```
|
|
115
116
|
USAGE
|
|
116
|
-
$ hd report committers [--json] [-
|
|
117
|
+
$ hd report committers [--json] [-x <value>...] [-d <value>] [--monthly] [-m <value> | -s <value> | -e <value> | | ]
|
|
118
|
+
[-c] [-s]
|
|
117
119
|
|
|
118
120
|
FLAGS
|
|
119
|
-
-c, --csv
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
--
|
|
121
|
+
-c, --csv Output in CSV format
|
|
122
|
+
-d, --directory=<value> Directory to search
|
|
123
|
+
-e, --afterDate=<value> [default: 2024-11-19] Start date (format: yyyy-MM-dd)
|
|
124
|
+
-m, --months=<value> [default: 12] The number of months of git history to review. Cannot be used along beforeDate
|
|
125
|
+
and afterDate
|
|
126
|
+
-s, --beforeDate=<value> [default: 2025-11-19] End date (format: yyyy-MM-dd)
|
|
127
|
+
-s, --save Save the committers report as herodevs.committers.<output>
|
|
128
|
+
-x, --exclude=<value>... Path Exclusions (eg -x="./src/bin" -x="./dist")
|
|
129
|
+
--json Output to JSON format
|
|
130
|
+
--monthly Break down by calendar month.
|
|
125
131
|
|
|
126
132
|
DESCRIPTION
|
|
127
133
|
Generate report of committers to a git repository
|
|
@@ -136,7 +142,7 @@ EXAMPLES
|
|
|
136
142
|
$ hd report committers --csv
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.
|
|
145
|
+
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/report/committers.ts)_
|
|
140
146
|
|
|
141
147
|
## `hd scan eol`
|
|
142
148
|
|
|
@@ -191,7 +197,41 @@ EXAMPLES
|
|
|
191
197
|
$ hd scan eol --json
|
|
192
198
|
```
|
|
193
199
|
|
|
194
|
-
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.
|
|
200
|
+
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/scan/eol.ts)_
|
|
201
|
+
|
|
202
|
+
## `hd tracker init`
|
|
203
|
+
|
|
204
|
+
Initialize the tracker configuration
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
USAGE
|
|
208
|
+
$ hd tracker init [--force -o] [-d <value>] [-f <value>] [-i <value>...]
|
|
209
|
+
|
|
210
|
+
FLAGS
|
|
211
|
+
-d, --outputDir=<value> [default: hd-tracker] Output directory for the tracker configuration file
|
|
212
|
+
-f, --configFile=<value> [default: config.json] Filename for the tracker configuration file
|
|
213
|
+
-i, --ignorePatterns=<value>... [default: node_modules] Ignore patterns to use for the tracker configuration file
|
|
214
|
+
-o, --overwrite Overwrites the tracker configuration file if it exists
|
|
215
|
+
--force Force tracker configuration file creation. Use with --overwrite flag
|
|
216
|
+
|
|
217
|
+
DESCRIPTION
|
|
218
|
+
Initialize the tracker configuration
|
|
219
|
+
|
|
220
|
+
EXAMPLES
|
|
221
|
+
$ hd tracker init
|
|
222
|
+
|
|
223
|
+
$ hd tracker init -d trackerDir
|
|
224
|
+
|
|
225
|
+
$ hd tracker init -d trackerDir -f configFileName
|
|
226
|
+
|
|
227
|
+
$ hd tracker init -i node_modules
|
|
228
|
+
|
|
229
|
+
$ hd tracker init -i node_modules -i custom_modules
|
|
230
|
+
|
|
231
|
+
$ hd tracker init -o
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
_See code: [src/commands/tracker/init.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/tracker/init.ts)_
|
|
195
235
|
|
|
196
236
|
## `hd update [CHANNEL]`
|
|
197
237
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { gql } from '@apollo/client/core
|
|
1
|
+
import { gql } from '@apollo/client/core';
|
|
2
2
|
export const createReportMutation = gql `
|
|
3
3
|
mutation createReport($input: CreateEolReportInput) {
|
|
4
4
|
eol {
|
|
@@ -20,6 +20,7 @@ query GetEolReport($input: GetEolReportInput) {
|
|
|
20
20
|
components {
|
|
21
21
|
purl
|
|
22
22
|
metadata
|
|
23
|
+
dependencySummary
|
|
23
24
|
nesRemediation {
|
|
24
25
|
remediations {
|
|
25
26
|
urls {
|
package/dist/api/nes.client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ApolloClient } from '@apollo/client/core
|
|
1
|
+
import { ApolloClient } from '@apollo/client/core';
|
|
2
2
|
import type { CreateEolReportInput, EolReport } from '@herodevs/eol-shared';
|
|
3
|
-
export declare const createApollo: (uri: string) => ApolloClient
|
|
3
|
+
export declare const createApollo: (uri: string) => ApolloClient;
|
|
4
4
|
export declare const SbomScanner: (client: ReturnType<typeof createApollo>) => (input: CreateEolReportInput) => Promise<EolReport>;
|
|
5
5
|
export declare class NesClient {
|
|
6
6
|
startScan: ReturnType<typeof SbomScanner>;
|
package/dist/api/nes.client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client/core
|
|
1
|
+
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client/core';
|
|
2
2
|
import { config } from "../config/constants.js";
|
|
3
3
|
import { debugLogger } from "../service/log.svc.js";
|
|
4
4
|
import { stripTypename } from "../utils/strip-typename.js";
|
|
@@ -18,12 +18,13 @@ export const createApollo = (uri) => new ApolloClient({
|
|
|
18
18
|
});
|
|
19
19
|
export const SbomScanner = (client) => {
|
|
20
20
|
return async (input) => {
|
|
21
|
-
|
|
21
|
+
let res;
|
|
22
|
+
res = await client.mutate({
|
|
22
23
|
mutation: createReportMutation,
|
|
23
24
|
variables: { input },
|
|
24
25
|
});
|
|
25
|
-
if (res?.errors
|
|
26
|
-
debugLogger('
|
|
26
|
+
if (res?.error || res?.errors) {
|
|
27
|
+
debugLogger('Error returned from createReport mutation: %o', res.error || res?.errors);
|
|
27
28
|
throw new Error('Failed to create EOL report');
|
|
28
29
|
}
|
|
29
30
|
const result = res.data?.eol?.createReport;
|
|
@@ -47,10 +48,12 @@ export const SbomScanner = (client) => {
|
|
|
47
48
|
let reportMetadata = null;
|
|
48
49
|
for (let i = 0; i < pages.length; i += config.concurrentPageRequests) {
|
|
49
50
|
const batch = pages.slice(i, i + config.concurrentPageRequests);
|
|
50
|
-
|
|
51
|
+
let batchResponses;
|
|
52
|
+
batchResponses = await Promise.all(batch);
|
|
51
53
|
for (const response of batchResponses) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
const queryErrors = response?.errors;
|
|
55
|
+
if (response?.error || queryErrors?.length || !response.data?.eol) {
|
|
56
|
+
debugLogger('Error in getReport query response: %o', response?.error ?? queryErrors ?? response);
|
|
54
57
|
throw new Error('Failed to fetch EOL report');
|
|
55
58
|
}
|
|
56
59
|
const report = response.data.eol.report;
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { type
|
|
2
|
+
import { type CommittersReport } from '../../service/committers.svc.ts';
|
|
3
3
|
export default class Committers extends Command {
|
|
4
4
|
static description: string;
|
|
5
5
|
static enableJsonFlag: boolean;
|
|
6
6
|
static examples: string[];
|
|
7
7
|
static flags: {
|
|
8
|
+
beforeDate: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
afterDate: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
exclude: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
directory: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
monthly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
14
|
months: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
15
|
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
16
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
17
|
};
|
|
12
|
-
run(): Promise<
|
|
13
|
-
/**
|
|
14
|
-
* Generates structured report data
|
|
15
|
-
* @param entries - parsed git log output for commits
|
|
16
|
-
*/
|
|
17
|
-
private generateReportData;
|
|
18
|
+
run(): Promise<CommittersReport | string>;
|
|
18
19
|
/**
|
|
19
20
|
* Fetches git commit data with month and author information
|
|
20
21
|
* @param sinceDate - Date range for git log
|
|
22
|
+
* @param beforeDateEndOfDay - End date for git log
|
|
23
|
+
* @param ignores - indicate elements to exclude for git log
|
|
24
|
+
* @param cwd - directory to use for git log
|
|
21
25
|
*/
|
|
22
26
|
private fetchGitCommitData;
|
|
23
27
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
3
|
import { Command, Flags } from '@oclif/core';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { makeTable } from '@oclif/table';
|
|
5
|
+
import { endOfDay, formatDate, formatISO, parse, subMonths } from 'date-fns';
|
|
6
|
+
import { DEFAULT_DATE_COMMIT_FORMAT, DEFAULT_DATE_FORMAT, filenamePrefix, GIT_OUTPUT_FORMAT, } from "../../config/constants.js";
|
|
7
|
+
import { generateCommittersReport, generateMonthlyReport, parseGitLogOutput, } from "../../service/committers.svc.js";
|
|
7
8
|
import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
|
|
8
9
|
export default class Committers extends Command {
|
|
9
10
|
static description = 'Generate report of committers to a git repository';
|
|
@@ -15,10 +16,39 @@ export default class Committers extends Command {
|
|
|
15
16
|
'<%= config.bin %> <%= command.id %> --csv',
|
|
16
17
|
];
|
|
17
18
|
static flags = {
|
|
19
|
+
beforeDate: Flags.string({
|
|
20
|
+
char: 's',
|
|
21
|
+
default: formatDate(new Date(), DEFAULT_DATE_FORMAT),
|
|
22
|
+
description: `End date (format: ${DEFAULT_DATE_FORMAT})`,
|
|
23
|
+
}),
|
|
24
|
+
afterDate: Flags.string({
|
|
25
|
+
char: 'e',
|
|
26
|
+
default: formatDate(subMonths(new Date(), 12), DEFAULT_DATE_FORMAT),
|
|
27
|
+
description: `Start date (format: ${DEFAULT_DATE_FORMAT})`,
|
|
28
|
+
}),
|
|
29
|
+
exclude: Flags.string({
|
|
30
|
+
char: 'x',
|
|
31
|
+
description: 'Path Exclusions (eg -x="./src/bin" -x="./dist")',
|
|
32
|
+
multiple: true,
|
|
33
|
+
multipleNonGreedy: true,
|
|
34
|
+
}),
|
|
35
|
+
json: Flags.boolean({
|
|
36
|
+
description: 'Output to JSON format',
|
|
37
|
+
default: false,
|
|
38
|
+
}),
|
|
39
|
+
directory: Flags.string({
|
|
40
|
+
char: 'd',
|
|
41
|
+
description: 'Directory to search',
|
|
42
|
+
}),
|
|
43
|
+
monthly: Flags.boolean({
|
|
44
|
+
description: 'Break down by calendar month.',
|
|
45
|
+
default: false,
|
|
46
|
+
}),
|
|
18
47
|
months: Flags.integer({
|
|
19
48
|
char: 'm',
|
|
20
|
-
description: 'The number of months of git history to review',
|
|
49
|
+
description: 'The number of months of git history to review. Cannot be used along beforeDate and afterDate',
|
|
21
50
|
default: 12,
|
|
51
|
+
exclusive: ['beforeDate', 'afterDate', 's', 'e'],
|
|
22
52
|
}),
|
|
23
53
|
csv: Flags.boolean({
|
|
24
54
|
char: 'c',
|
|
@@ -33,100 +63,138 @@ export default class Committers extends Command {
|
|
|
33
63
|
};
|
|
34
64
|
async run() {
|
|
35
65
|
const { flags } = await this.parse(Committers);
|
|
36
|
-
const { months, csv, save } = flags;
|
|
66
|
+
const { afterDate, beforeDate, exclude, directory: cwd, monthly, months, csv, save } = flags;
|
|
37
67
|
const isJson = this.jsonEnabled();
|
|
38
|
-
const
|
|
39
|
-
|
|
68
|
+
const reportFormat = isJson ? 'json' : csv ? 'csv' : 'txt';
|
|
69
|
+
const afterDateStartOfDay = months
|
|
70
|
+
? `${subMonths(new Date(), months)}`
|
|
71
|
+
: `${parse(afterDate, DEFAULT_DATE_FORMAT, new Date())}`;
|
|
72
|
+
const beforeDateEndOfDay = formatISO(endOfDay(parse(beforeDate, DEFAULT_DATE_FORMAT, new Date())));
|
|
73
|
+
const ignores = exclude && exclude.length > 0 ? `. "!(${exclude.join('|')})"` : undefined;
|
|
40
74
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
75
|
+
const entries = this.fetchGitCommitData(afterDateStartOfDay, beforeDateEndOfDay, ignores, cwd);
|
|
76
|
+
if (entries.length === 0) {
|
|
77
|
+
return `No commits found between ${afterDate} and ${beforeDate}`;
|
|
58
78
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
this.log('\nFetched %d commit entries\n', entries.length);
|
|
80
|
+
const reportData = monthly ? generateMonthlyReport(entries) : generateCommittersReport(entries);
|
|
81
|
+
let finalReport;
|
|
82
|
+
switch (reportFormat) {
|
|
83
|
+
case 'json':
|
|
84
|
+
finalReport = JSON.stringify(reportData.map((row) => 'month' in row
|
|
85
|
+
? {
|
|
86
|
+
month: row.month,
|
|
87
|
+
start: row.start,
|
|
88
|
+
end: row.end,
|
|
89
|
+
committers: row.committers,
|
|
90
|
+
}
|
|
91
|
+
: {
|
|
92
|
+
name: row.author,
|
|
93
|
+
count: row.commits.length,
|
|
94
|
+
lastCommitDate: formatDate(row.lastCommitOn, DEFAULT_DATE_COMMIT_FORMAT),
|
|
95
|
+
}), null, 2);
|
|
96
|
+
break;
|
|
97
|
+
case 'csv':
|
|
98
|
+
finalReport = reportData
|
|
99
|
+
.map((row, index) => 'month' in row
|
|
100
|
+
? `${index},${row.month},${row.start},${row.end},${row.totalCommits}`
|
|
101
|
+
: `${index},${row.author},${row.commits.length},${formatDate(row.lastCommitOn, DEFAULT_DATE_COMMIT_FORMAT).replace(',', '')}`)
|
|
102
|
+
.join('\n')
|
|
103
|
+
.replace(/^/, monthly ? `(index),month,start,end,totalCommits\n` : `(index),Committer,Commits,Last Commit Date\n`);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
if (monthly) {
|
|
107
|
+
finalReport = makeTable({
|
|
108
|
+
title: 'Monthly Report',
|
|
109
|
+
data: reportData
|
|
110
|
+
.filter((row) => 'month' in row)
|
|
111
|
+
.map((row, index) => ({
|
|
112
|
+
index,
|
|
113
|
+
month: row.month,
|
|
114
|
+
start: row.start,
|
|
115
|
+
end: row.end,
|
|
116
|
+
totalCommits: row.totalCommits,
|
|
117
|
+
})),
|
|
118
|
+
headerOptions: {
|
|
119
|
+
color: undefined,
|
|
120
|
+
bold: false,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
67
123
|
}
|
|
68
|
-
|
|
69
|
-
|
|
124
|
+
else {
|
|
125
|
+
finalReport = makeTable({
|
|
126
|
+
title: 'Committers Report',
|
|
127
|
+
data: reportData
|
|
128
|
+
.filter((row) => 'author' in row)
|
|
129
|
+
.map((row, index) => ({
|
|
130
|
+
index,
|
|
131
|
+
author: row.author,
|
|
132
|
+
commits: row.commits.length,
|
|
133
|
+
lastCommitOn: formatDate(row.lastCommitOn, DEFAULT_DATE_COMMIT_FORMAT),
|
|
134
|
+
})),
|
|
135
|
+
columns: [
|
|
136
|
+
{
|
|
137
|
+
key: 'index',
|
|
138
|
+
name: '(index)',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: 'author',
|
|
142
|
+
name: 'Committer',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'commits',
|
|
146
|
+
name: 'Commits',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: 'lastCommitOn',
|
|
150
|
+
name: 'Last Commit Date',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
headerOptions: {
|
|
154
|
+
color: undefined,
|
|
155
|
+
bold: false,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
70
158
|
}
|
|
71
|
-
|
|
72
|
-
else {
|
|
73
|
-
this.log(textOutput);
|
|
74
|
-
}
|
|
75
|
-
return csvOutput;
|
|
159
|
+
break;
|
|
76
160
|
}
|
|
77
161
|
if (save) {
|
|
78
162
|
try {
|
|
79
|
-
fs.writeFileSync(
|
|
80
|
-
|
|
163
|
+
fs.writeFileSync(`${filenamePrefix}.${monthly ? 'monthly' : 'committers'}.${reportFormat}`, finalReport, {
|
|
164
|
+
encoding: 'utf-8',
|
|
165
|
+
});
|
|
166
|
+
this.log(`Report written to ${reportFormat.toUpperCase()}`);
|
|
81
167
|
}
|
|
82
|
-
catch (
|
|
83
|
-
this.error(`Failed to save
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.error(`Failed to save ${reportFormat.toUpperCase()} report: ${getErrorMessage(err)}`);
|
|
84
170
|
}
|
|
85
171
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
return textOutput;
|
|
172
|
+
this.log(finalReport);
|
|
173
|
+
return finalReport;
|
|
90
174
|
}
|
|
91
175
|
catch (error) {
|
|
92
176
|
this.error(`Failed to generate report: ${getErrorMessage(error)}`);
|
|
93
177
|
}
|
|
94
178
|
}
|
|
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
179
|
/**
|
|
119
180
|
* Fetches git commit data with month and author information
|
|
120
181
|
* @param sinceDate - Date range for git log
|
|
182
|
+
* @param beforeDateEndOfDay - End date for git log
|
|
183
|
+
* @param ignores - indicate elements to exclude for git log
|
|
184
|
+
* @param cwd - directory to use for git log
|
|
121
185
|
*/
|
|
122
|
-
fetchGitCommitData(sinceDate) {
|
|
123
|
-
const
|
|
186
|
+
fetchGitCommitData(sinceDate, beforeDateEndOfDay, ignores, cwd) {
|
|
187
|
+
const logParameters = [
|
|
124
188
|
'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
189
|
`--since="${sinceDate}"`,
|
|
129
|
-
|
|
190
|
+
`--until="${beforeDateEndOfDay}"`,
|
|
191
|
+
`--format=${GIT_OUTPUT_FORMAT}`,
|
|
192
|
+
...(cwd ? ['--', cwd] : []),
|
|
193
|
+
...(ignores ? ['--', ignores] : []),
|
|
194
|
+
];
|
|
195
|
+
const logProcess = spawnSync('git', logParameters, {
|
|
196
|
+
encoding: 'utf-8',
|
|
197
|
+
});
|
|
130
198
|
if (logProcess.error) {
|
|
131
199
|
if (isErrnoException(logProcess.error)) {
|
|
132
200
|
if (logProcess.error.code === 'ENOENT') {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Init extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static enableJsonFlag: boolean;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
overwrite: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
outputDir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
configFile: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
ignorePatterns: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Command, Flags } from '@oclif/core';
|
|
3
|
+
import { TRACKER_DEFAULT_CONFIG } from '../../config/tracker.config.js';
|
|
4
|
+
import { createTrackerConfig, getRootDir } from '../../service/tracker.svc.js';
|
|
5
|
+
export default class Init extends Command {
|
|
6
|
+
static description = 'Initialize the tracker configuration';
|
|
7
|
+
static enableJsonFlag = false;
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> -d trackerDir',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> -d trackerDir -f configFileName',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> -i node_modules',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> -i node_modules -i custom_modules',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> -o',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
overwrite: Flags.boolean({
|
|
18
|
+
char: 'o',
|
|
19
|
+
description: 'Overwrites the tracker configuration file if it exists',
|
|
20
|
+
}),
|
|
21
|
+
force: Flags.boolean({
|
|
22
|
+
description: 'Force tracker configuration file creation. Use with --overwrite flag',
|
|
23
|
+
dependsOn: ['overwrite'],
|
|
24
|
+
}),
|
|
25
|
+
outputDir: Flags.string({
|
|
26
|
+
char: 'd',
|
|
27
|
+
description: 'Output directory for the tracker configuration file',
|
|
28
|
+
default: 'hd-tracker',
|
|
29
|
+
}),
|
|
30
|
+
configFile: Flags.string({
|
|
31
|
+
char: 'f',
|
|
32
|
+
description: 'Filename for the tracker configuration file',
|
|
33
|
+
default: 'config.json',
|
|
34
|
+
}),
|
|
35
|
+
ignorePatterns: Flags.string({
|
|
36
|
+
char: 'i',
|
|
37
|
+
description: 'Ignore patterns to use for the tracker configuration file',
|
|
38
|
+
multiple: true,
|
|
39
|
+
multipleNonGreedy: true,
|
|
40
|
+
default: ['node_modules'],
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
async run() {
|
|
44
|
+
const { flags } = await this.parse(Init);
|
|
45
|
+
const { overwrite, outputDir, configFile, ignorePatterns, force } = flags;
|
|
46
|
+
this.log('Starting tracker init command');
|
|
47
|
+
if (overwrite) {
|
|
48
|
+
if (force) {
|
|
49
|
+
this.warn(`You're using the --force flag along the --overwrite flag.`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const response = await confirm({
|
|
53
|
+
message: `You're using the overwrite flag. If a previous configuration file exists, it will be replaced. Do you want to continue?`,
|
|
54
|
+
default: false,
|
|
55
|
+
});
|
|
56
|
+
this.log(response ? 'Yes' : 'No');
|
|
57
|
+
if (!response) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const rootDir = getRootDir(global.process.cwd());
|
|
64
|
+
const outputConfig = {
|
|
65
|
+
...TRACKER_DEFAULT_CONFIG,
|
|
66
|
+
outputDir,
|
|
67
|
+
configFile,
|
|
68
|
+
ignorePatterns,
|
|
69
|
+
};
|
|
70
|
+
await createTrackerConfig(rootDir, outputConfig, overwrite);
|
|
71
|
+
this.log(`Tracker init command completed successfully.`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err instanceof Error) {
|
|
75
|
+
this.error(err, {
|
|
76
|
+
message: err.message,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.error('An unknown error occurred while running the tracker init command');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -4,6 +4,10 @@ export declare const GRAPHQL_PATH = "/graphql";
|
|
|
4
4
|
export declare const ANALYTICS_URL = "https://apps.herodevs.com/api/eol/track";
|
|
5
5
|
export declare const CONCURRENT_PAGE_REQUESTS = 3;
|
|
6
6
|
export declare const PAGE_SIZE = 500;
|
|
7
|
+
export declare const GIT_OUTPUT_FORMAT: string;
|
|
8
|
+
export declare const DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
|
9
|
+
export declare const DEFAULT_DATE_COMMIT_FORMAT = "MM/dd/yyyy, h:mm:ss a";
|
|
10
|
+
export declare const DEFAULT_DATE_COMMIT_MONTH_FORMAT = "MMMM yyyy";
|
|
7
11
|
export declare const config: {
|
|
8
12
|
eolReportUrl: string;
|
|
9
13
|
graphqlHost: string;
|
package/dist/config/constants.js
CHANGED
|
@@ -4,6 +4,11 @@ export const GRAPHQL_PATH = '/graphql';
|
|
|
4
4
|
export const ANALYTICS_URL = 'https://apps.herodevs.com/api/eol/track';
|
|
5
5
|
export const CONCURRENT_PAGE_REQUESTS = 3;
|
|
6
6
|
export const PAGE_SIZE = 500;
|
|
7
|
+
export const GIT_OUTPUT_FORMAT = `"${['%h', '%an', '%ad'].join('|')}"`;
|
|
8
|
+
// Committers Report - Date Constants
|
|
9
|
+
export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd';
|
|
10
|
+
export const DEFAULT_DATE_COMMIT_FORMAT = 'MM/dd/yyyy, h:mm:ss a';
|
|
11
|
+
export const DEFAULT_DATE_COMMIT_MONTH_FORMAT = 'MMMM yyyy';
|
|
7
12
|
let concurrentPageRequests = CONCURRENT_PAGE_REQUESTS;
|
|
8
13
|
const parsed = Number.parseInt(process.env.CONCURRENT_PAGE_REQUESTS ?? '0', 10);
|
|
9
14
|
if (parsed > 0) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TrackerCategoryDefinition {
|
|
2
|
+
fileTypes: string[];
|
|
3
|
+
includes: string[];
|
|
4
|
+
excludes?: string[];
|
|
5
|
+
jsTsPairs?: 'js' | 'ts' | 'ignore';
|
|
6
|
+
}
|
|
7
|
+
export type TrackerConfig = {
|
|
8
|
+
categories: {
|
|
9
|
+
[key: string]: TrackerCategoryDefinition;
|
|
10
|
+
};
|
|
11
|
+
ignorePatterns?: string[];
|
|
12
|
+
outputDir: string;
|
|
13
|
+
configFile: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const TRACKER_ROOT_FILE = "package.json";
|
|
16
|
+
export declare const TRACKER_DEFAULT_CONFIG: TrackerConfig;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const TRACKER_ROOT_FILE = 'package.json';
|
|
2
|
+
export const TRACKER_DEFAULT_CONFIG = {
|
|
3
|
+
categories: {
|
|
4
|
+
legacy: {
|
|
5
|
+
fileTypes: ['js', 'ts', 'html', 'css', 'scss', 'less'],
|
|
6
|
+
includes: ['./legacy'],
|
|
7
|
+
jsTsPairs: 'js',
|
|
8
|
+
},
|
|
9
|
+
modern: {
|
|
10
|
+
fileTypes: ['ts', 'html', 'css', 'scss', 'less'],
|
|
11
|
+
includes: ['./modern'],
|
|
12
|
+
jsTsPairs: 'ts',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
ignorePatterns: ['node_modules'],
|
|
16
|
+
outputDir: 'hd-tracker',
|
|
17
|
+
configFile: 'config.json',
|
|
18
|
+
};
|
|
@@ -1,25 +1,43 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export type ReportFormat = 'txt' | 'csv' | 'json';
|
|
2
|
+
export type CommitEntry = {
|
|
3
|
+
commitHash: string;
|
|
3
4
|
author: string;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
date: Date;
|
|
6
|
+
monthGroup: string;
|
|
7
|
+
};
|
|
8
|
+
export type CommitAuthorData = {
|
|
9
|
+
commits: CommitEntry[];
|
|
10
|
+
lastCommitOn: Date;
|
|
11
|
+
};
|
|
12
|
+
export type CommitMonthData = {
|
|
13
|
+
start: Date | string;
|
|
14
|
+
end: Date | string;
|
|
15
|
+
totalCommits: number;
|
|
16
|
+
committers: AuthorCommitCount;
|
|
17
|
+
};
|
|
18
|
+
export type AuthorCommitCount = {
|
|
6
19
|
[author: string]: number;
|
|
7
|
-
}
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
};
|
|
21
|
+
export type AuthorReportTableRow = {
|
|
22
|
+
index: number;
|
|
23
|
+
author: string;
|
|
24
|
+
commits: number;
|
|
25
|
+
lastCommitOn: string;
|
|
26
|
+
};
|
|
27
|
+
export type MonthlyReportTableRow = {
|
|
28
|
+
index: number;
|
|
29
|
+
month: number;
|
|
30
|
+
start: string;
|
|
31
|
+
end: string;
|
|
32
|
+
totalCommits: number;
|
|
33
|
+
};
|
|
34
|
+
export type MonthlyReportRow = {
|
|
35
|
+
month: string;
|
|
36
|
+
} & CommitMonthData;
|
|
37
|
+
export type AuthorReportRow = {
|
|
38
|
+
author: string;
|
|
39
|
+
} & CommitAuthorData;
|
|
40
|
+
export type CommittersReport = AuthorReportRow[] | MonthlyReportRow[];
|
|
23
41
|
/**
|
|
24
42
|
* Parses git log output into structured data
|
|
25
43
|
* @param output - Git log command output
|
|
@@ -27,44 +45,14 @@ export interface ReportData {
|
|
|
27
45
|
*/
|
|
28
46
|
export declare function parseGitLogOutput(output: string): CommitEntry[];
|
|
29
47
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param entries -
|
|
32
|
-
* @returns
|
|
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
|
|
48
|
+
* Generates commits author report
|
|
49
|
+
* @param entries - commit entries from git log
|
|
50
|
+
* @returns Commits Author Report
|
|
62
51
|
*/
|
|
63
|
-
export declare function
|
|
52
|
+
export declare function generateCommittersReport(entries: CommitEntry[]): AuthorReportRow[];
|
|
64
53
|
/**
|
|
65
|
-
*
|
|
66
|
-
* @param
|
|
67
|
-
* @
|
|
68
|
-
* @returns
|
|
54
|
+
* Generates commits monthly report
|
|
55
|
+
* @param entries - commit entries from git log
|
|
56
|
+
* @returns Monthly Report
|
|
69
57
|
*/
|
|
70
|
-
export declare function
|
|
58
|
+
export declare function generateMonthlyReport(entries: CommitEntry[]): MonthlyReportRow[];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { endOfMonth, formatDate, parse } from 'date-fns';
|
|
2
|
+
import { DEFAULT_DATE_COMMIT_MONTH_FORMAT, DEFAULT_DATE_FORMAT } from '../config/constants.js';
|
|
1
3
|
/**
|
|
2
4
|
* Parses git log output into structured data
|
|
3
5
|
* @param output - Git log command output
|
|
@@ -9,188 +11,68 @@ export function parseGitLogOutput(output) {
|
|
|
9
11
|
.filter(Boolean)
|
|
10
12
|
.map((line) => {
|
|
11
13
|
// Remove surrounding double quotes if present (e.g. "March|John Doe" → March|John Doe)
|
|
12
|
-
const [
|
|
13
|
-
return {
|
|
14
|
+
const [commitHash, author, date] = line.replace(/^"(.*)"$/, '$1').split('|');
|
|
15
|
+
return {
|
|
16
|
+
commitHash,
|
|
17
|
+
author,
|
|
18
|
+
date: parse(formatDate(new Date(date), DEFAULT_DATE_FORMAT), DEFAULT_DATE_FORMAT, new Date()),
|
|
19
|
+
monthGroup: formatDate(new Date(date), DEFAULT_DATE_COMMIT_MONTH_FORMAT),
|
|
20
|
+
};
|
|
14
21
|
});
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param entries -
|
|
19
|
-
* @returns
|
|
24
|
+
* Generates commits author report
|
|
25
|
+
* @param entries - commit entries from git log
|
|
26
|
+
* @returns Commits Author Report
|
|
20
27
|
*/
|
|
21
|
-
export function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
acc
|
|
28
|
+
export function generateCommittersReport(entries) {
|
|
29
|
+
return Array.from(entries
|
|
30
|
+
.sort((a, b) => b.date.valueOf() - a.date.valueOf())
|
|
31
|
+
.reduce((acc, curr, _index, array) => {
|
|
32
|
+
if (!acc.has(curr.author)) {
|
|
33
|
+
const byAuthor = array.filter((c) => c.author === curr.author);
|
|
34
|
+
acc.set(curr.author, {
|
|
35
|
+
commits: byAuthor,
|
|
36
|
+
lastCommitOn: byAuthor[0].date,
|
|
37
|
+
});
|
|
28
38
|
}
|
|
29
|
-
acc[monthKey].push(entry);
|
|
30
39
|
return acc;
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
|
40
|
+
}, new Map()))
|
|
41
|
+
.map(([key, value]) => ({
|
|
42
|
+
author: key,
|
|
43
|
+
commits: value.commits,
|
|
44
|
+
lastCommitOn: value.lastCommitOn,
|
|
45
|
+
}))
|
|
46
|
+
.sort((a, b) => b.commits.length - a.commits.length);
|
|
54
47
|
}
|
|
55
48
|
/**
|
|
56
|
-
*
|
|
57
|
-
* @param entries -
|
|
58
|
-
* @returns
|
|
49
|
+
* Generates commits monthly report
|
|
50
|
+
* @param entries - commit entries from git log
|
|
51
|
+
* @returns Monthly Report
|
|
59
52
|
*/
|
|
60
|
-
export function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
export function generateMonthlyReport(entries) {
|
|
54
|
+
return Array.from(entries
|
|
55
|
+
.sort((a, b) => b.date.valueOf() - a.date.valueOf())
|
|
56
|
+
.reduce((acc, curr, _index, array) => {
|
|
57
|
+
if (!acc.has(curr.monthGroup)) {
|
|
58
|
+
const monthlyCommits = array.filter((e) => e.monthGroup === curr.monthGroup);
|
|
59
|
+
acc.set(curr.monthGroup, {
|
|
60
|
+
start: formatDate(monthlyCommits[0].date, DEFAULT_DATE_FORMAT),
|
|
61
|
+
end: formatDate(endOfMonth(monthlyCommits[0].date), DEFAULT_DATE_FORMAT),
|
|
62
|
+
totalCommits: monthlyCommits.length,
|
|
63
|
+
committers: monthlyCommits.reduce((acc, curr) => {
|
|
64
|
+
if (!acc[curr.author]) {
|
|
65
|
+
acc[curr.author] = monthlyCommits.filter((c) => c.author === curr.author).length;
|
|
66
|
+
}
|
|
67
|
+
return acc;
|
|
68
|
+
}, {}),
|
|
69
|
+
});
|
|
65
70
|
}
|
|
66
|
-
acc[authorKey].push(entry);
|
|
67
71
|
return acc;
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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;
|
|
72
|
+
}, new Map()))
|
|
73
|
+
.map(([key, value]) => ({
|
|
74
|
+
month: key,
|
|
75
|
+
...value,
|
|
76
|
+
}))
|
|
77
|
+
.sort((a, b) => new Date(a.end).valueOf() - new Date(b.end).valueOf());
|
|
196
78
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { TRACKER_ROOT_FILE } from '../config/tracker.config.js';
|
|
5
|
+
export const getRootDir = (path) => {
|
|
6
|
+
if (existsSync(join(path, TRACKER_ROOT_FILE))) {
|
|
7
|
+
return path;
|
|
8
|
+
}
|
|
9
|
+
else if (path === join(path, '..')) {
|
|
10
|
+
throw new Error(`Couldn't find root directory for the project`);
|
|
11
|
+
}
|
|
12
|
+
return getRootDir(resolve(join(path, '..')));
|
|
13
|
+
};
|
|
14
|
+
export const createTrackerConfig = async (rootPath, config, overwrite = false) => {
|
|
15
|
+
const { outputDir } = config;
|
|
16
|
+
const configDir = join(rootPath, outputDir);
|
|
17
|
+
const configFile = join(configDir, config.configFile);
|
|
18
|
+
const doesConfigFileExists = existsSync(configFile);
|
|
19
|
+
if (!existsSync(configDir)) {
|
|
20
|
+
mkdirSync(configDir);
|
|
21
|
+
}
|
|
22
|
+
if (doesConfigFileExists && !overwrite) {
|
|
23
|
+
throw new Error(`Configuration file already exists for this repo. If you want to overwrite it, run the command again with the --overwrite flag`);
|
|
24
|
+
}
|
|
25
|
+
await writeFile(join(configDir, config.configFile), JSON.stringify(config, null, 2));
|
|
26
|
+
};
|
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.14",
|
|
4
4
|
"author": "HeroDevs, Inc",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "./bin/run.js"
|
|
@@ -39,13 +39,16 @@
|
|
|
39
39
|
"herodevs cli"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@amplitude/analytics-node": "^1.5.
|
|
43
|
-
"@apollo/client": "^
|
|
42
|
+
"@amplitude/analytics-node": "^1.5.22",
|
|
43
|
+
"@apollo/client": "^4.0.9",
|
|
44
44
|
"@cyclonedx/cdxgen": "^11.11.0",
|
|
45
45
|
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.12",
|
|
46
|
+
"@inquirer/prompts": "^8.0.1",
|
|
46
47
|
"@oclif/core": "^4.8.0",
|
|
47
48
|
"@oclif/plugin-help": "^6.2.32",
|
|
48
|
-
"@oclif/plugin-update": "^4.7.
|
|
49
|
+
"@oclif/plugin-update": "^4.7.14",
|
|
50
|
+
"@oclif/table": "^0.5.1",
|
|
51
|
+
"date-fns": "^4.1.0",
|
|
49
52
|
"node-machine-id": "^1.1.12",
|
|
50
53
|
"ora": "^9.0.0",
|
|
51
54
|
"packageurl-js": "^2.0.1",
|
|
@@ -53,14 +56,16 @@
|
|
|
53
56
|
"update-notifier": "^7.3.1"
|
|
54
57
|
},
|
|
55
58
|
"devDependencies": {
|
|
56
|
-
"@biomejs/biome": "^2.3.
|
|
59
|
+
"@biomejs/biome": "^2.3.4",
|
|
57
60
|
"@oclif/test": "^4.1.13",
|
|
58
61
|
"@types/inquirer": "^9.0.9",
|
|
62
|
+
"@types/mock-fs": "^4.13.4",
|
|
59
63
|
"@types/node": "^24.10.0",
|
|
60
64
|
"@types/sinon": "^17.0.4",
|
|
61
65
|
"@types/update-notifier": "^6.0.8",
|
|
62
66
|
"globstar": "^1.0.0",
|
|
63
|
-
"
|
|
67
|
+
"mock-fs": "^5.5.0",
|
|
68
|
+
"oclif": "^4.22.47",
|
|
64
69
|
"shx": "^0.4.0",
|
|
65
70
|
"sinon": "^21.0.0",
|
|
66
71
|
"ts-node": "^10.9.2",
|
|
@@ -110,4 +115,4 @@
|
|
|
110
115
|
}
|
|
111
116
|
},
|
|
112
117
|
"types": "dist/index.d.ts"
|
|
113
|
-
}
|
|
118
|
+
}
|