@herodevs/cli 2.0.0-beta.11 → 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 +65 -26
- package/dist/commands/report/committers.d.ts +23 -0
- package/dist/commands/report/committers.js +147 -0
- package/dist/service/cdx.svc.d.ts +9 -1
- package/dist/service/cdx.svc.js +17 -12
- 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 +12 -13
- package/dist/service/sbom.worker.d.ts +0 -1
- package/dist/service/sbom.worker.js +0 -26
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
|
|
@@ -209,6 +243,8 @@ You can use `@herodevs/cli` in your CI/CD pipelines to automate EOL scanning.
|
|
|
209
243
|
We provide a Docker image that's pre-configured to run EOL scans. Based on [`cdxgen`](https://github.com/CycloneDX/cdxgen),
|
|
210
244
|
it contains build tools for most project types and will provide best results when generating an SBOM. Use these templates to generate a report and save it to your CI job artifact for analysis and processing after your scan runs.
|
|
211
245
|
|
|
246
|
+
**Note:** There is a potential to run into permission issues writing out the report to your CI runner. Please ensure that your CI runner is setup to have proper read/write permissions for wherever your output files are being written to.
|
|
247
|
+
|
|
212
248
|
#### GitHub Actions
|
|
213
249
|
|
|
214
250
|
```yaml
|
|
@@ -227,20 +263,22 @@ jobs:
|
|
|
227
263
|
environment: demo
|
|
228
264
|
steps:
|
|
229
265
|
- name: Checkout repository
|
|
230
|
-
uses: actions/checkout@
|
|
266
|
+
uses: actions/checkout@v5
|
|
231
267
|
|
|
232
268
|
- name: Run EOL Scan
|
|
233
269
|
run: |
|
|
234
|
-
docker run --
|
|
270
|
+
docker run --name eol-scanner \
|
|
235
271
|
-v $GITHUB_WORKSPACE:/app \
|
|
236
272
|
-w /app \
|
|
237
|
-
ghcr.io/herodevs/eol-scan --save
|
|
273
|
+
ghcr.io/herodevs/eol-scan --save --output /tmp/herodevs.report.json
|
|
274
|
+
docker cp eol-scanner:/tmp/herodevs.report.json ${{ runner.temp }}/herodevs.report.json
|
|
275
|
+
docker rm eol-scanner
|
|
238
276
|
|
|
239
277
|
- name: Upload artifact
|
|
240
|
-
uses: actions/upload-artifact@
|
|
278
|
+
uses: actions/upload-artifact@v5
|
|
241
279
|
with:
|
|
242
280
|
name: my-eol-report
|
|
243
|
-
path:
|
|
281
|
+
path: ${{ runner.temp }}/herodevs.report.json
|
|
244
282
|
```
|
|
245
283
|
|
|
246
284
|
#### GitLab CI/CD
|
|
@@ -283,10 +321,11 @@ jobs:
|
|
|
283
321
|
scan:
|
|
284
322
|
runs-on: ubuntu-latest
|
|
285
323
|
steps:
|
|
286
|
-
- uses: actions/checkout@
|
|
287
|
-
|
|
324
|
+
- uses: actions/checkout@v5
|
|
325
|
+
|
|
326
|
+
- uses: actions/setup-node@v6
|
|
288
327
|
with:
|
|
289
|
-
node-version: '
|
|
328
|
+
node-version: '24'
|
|
290
329
|
|
|
291
330
|
- run: echo # Prepare environment, install tooling, perform setup, etc.
|
|
292
331
|
|
|
@@ -294,7 +333,7 @@ jobs:
|
|
|
294
333
|
run: npx @herodevs/cli@beta scan eol
|
|
295
334
|
|
|
296
335
|
- name: Upload artifact
|
|
297
|
-
uses: actions/upload-artifact@
|
|
336
|
+
uses: actions/upload-artifact@v5
|
|
298
337
|
with:
|
|
299
338
|
name: my-eol-report
|
|
300
339
|
path: herodevs.report.json
|
|
@@ -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
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createBom } from '@cyclonedx/cdxgen';
|
|
2
|
+
import { postProcess } from '@cyclonedx/cdxgen/stages/postgen/postgen';
|
|
1
3
|
import type { CdxBom } from '@herodevs/eol-shared';
|
|
2
4
|
export declare const SBOM_DEFAULT__OPTIONS: {
|
|
3
5
|
$0: string;
|
|
@@ -61,4 +63,10 @@ export declare const SBOM_DEFAULT__OPTIONS: {
|
|
|
61
63
|
* Lazy loads cdxgen (for ESM purposes), scans
|
|
62
64
|
* `directory`, and returns the `bomJson` property.
|
|
63
65
|
*/
|
|
64
|
-
|
|
66
|
+
type CreateSbomDependencies = {
|
|
67
|
+
createBom: typeof createBom;
|
|
68
|
+
postProcess: typeof postProcess;
|
|
69
|
+
};
|
|
70
|
+
export declare function createSbomFactory({ createBom: createBomDependency, postProcess: postProcessDependency, }?: Partial<CreateSbomDependencies>): (directory: string) => Promise<CdxBom>;
|
|
71
|
+
export declare const createSbom: (directory: string) => Promise<CdxBom>;
|
|
72
|
+
export {};
|
package/dist/service/cdx.svc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createBom } from '@cyclonedx/cdxgen';
|
|
2
|
+
import { postProcess } from '@cyclonedx/cdxgen/stages/postgen/postgen';
|
|
2
3
|
import { debugLogger } from "./log.svc.js";
|
|
3
4
|
const author = process.env.npm_package_author ?? 'HeroDevs, Inc.';
|
|
4
5
|
export const SBOM_DEFAULT__OPTIONS = {
|
|
@@ -24,8 +25,8 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
24
25
|
includeFormulation: false,
|
|
25
26
|
'no-install-deps': true,
|
|
26
27
|
noInstallDeps: true,
|
|
27
|
-
'min-confidence': 1,
|
|
28
|
-
minConfidence: 1,
|
|
28
|
+
'min-confidence': 0.1,
|
|
29
|
+
minConfidence: 0.1,
|
|
29
30
|
multiProject: true,
|
|
30
31
|
'no-banner': false,
|
|
31
32
|
noBabel: false,
|
|
@@ -62,14 +63,18 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
62
63
|
usagesSlicesFile: 'usages.slices.json',
|
|
63
64
|
validate: true,
|
|
64
65
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
export function createSbomFactory({ createBom: createBomDependency = createBom, postProcess: postProcessDependency = postProcess, } = {}) {
|
|
67
|
+
return async function createSbom(directory) {
|
|
68
|
+
const sbom = await createBomDependency(directory, SBOM_DEFAULT__OPTIONS);
|
|
69
|
+
if (!sbom) {
|
|
70
|
+
throw new Error('SBOM not generated');
|
|
71
|
+
}
|
|
72
|
+
const postProcessedSbom = postProcessDependency(sbom, SBOM_DEFAULT__OPTIONS);
|
|
73
|
+
if (!postProcessedSbom) {
|
|
74
|
+
throw new Error('SBOM not generated');
|
|
75
|
+
}
|
|
76
|
+
debugLogger('Successfully generated SBOM');
|
|
77
|
+
return postProcessedSbom.bomJson;
|
|
78
|
+
};
|
|
75
79
|
}
|
|
80
|
+
export const createSbom = createSbomFactory();
|
|
@@ -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,33 +39,32 @@
|
|
|
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
|
-
"@cyclonedx/cdxgen": "
|
|
45
|
-
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.
|
|
46
|
-
"@oclif/core": "^4.
|
|
44
|
+
"@cyclonedx/cdxgen": "^11.11.0",
|
|
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
|
-
"@oclif/plugin-update": "^4.7.
|
|
49
|
-
"graphql": "^16.11.0",
|
|
48
|
+
"@oclif/plugin-update": "^4.7.13",
|
|
50
49
|
"node-machine-id": "^1.1.12",
|
|
51
|
-
"ora": "^
|
|
50
|
+
"ora": "^9.0.0",
|
|
52
51
|
"packageurl-js": "^2.0.1",
|
|
53
52
|
"terminal-link": "^5.0.0",
|
|
54
53
|
"update-notifier": "^7.3.1"
|
|
55
54
|
},
|
|
56
55
|
"devDependencies": {
|
|
57
|
-
"@biomejs/biome": "^2.
|
|
56
|
+
"@biomejs/biome": "^2.3.3",
|
|
58
57
|
"@oclif/test": "^4.1.13",
|
|
59
58
|
"@types/inquirer": "^9.0.9",
|
|
60
|
-
"@types/node": "^24.
|
|
59
|
+
"@types/node": "^24.10.0",
|
|
61
60
|
"@types/sinon": "^17.0.4",
|
|
62
61
|
"@types/update-notifier": "^6.0.8",
|
|
63
62
|
"globstar": "^1.0.0",
|
|
64
|
-
"oclif": "^4.22.
|
|
63
|
+
"oclif": "^4.22.38",
|
|
65
64
|
"shx": "^0.4.0",
|
|
66
65
|
"sinon": "^21.0.0",
|
|
67
66
|
"ts-node": "^10.9.2",
|
|
68
|
-
"tsx": "^4.20.
|
|
67
|
+
"tsx": "^4.20.6",
|
|
69
68
|
"typescript": "^5.9.3"
|
|
70
69
|
},
|
|
71
70
|
"engines": {
|
|
@@ -111,4 +110,4 @@
|
|
|
111
110
|
}
|
|
112
111
|
},
|
|
113
112
|
"types": "dist/index.d.ts"
|
|
114
|
-
}
|
|
113
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { createBom } from '@cyclonedx/cdxgen';
|
|
4
|
-
import { filenamePrefix } from "../config/constants.js";
|
|
5
|
-
import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
|
|
6
|
-
process.on('uncaughtException', (err) => {
|
|
7
|
-
console.error('Uncaught exception:', err.message);
|
|
8
|
-
process.exit(1);
|
|
9
|
-
});
|
|
10
|
-
process.on('unhandledRejection', (reason) => {
|
|
11
|
-
console.error('Unhandled rejection:', reason);
|
|
12
|
-
process.exit(1);
|
|
13
|
-
});
|
|
14
|
-
try {
|
|
15
|
-
console.log('Sbom worker started');
|
|
16
|
-
const options = JSON.parse(process.argv[2]);
|
|
17
|
-
const { path, opts } = options;
|
|
18
|
-
const { bomJson } = await createBom(path, { ...SBOM_DEFAULT__OPTIONS, ...opts });
|
|
19
|
-
const outputPath = join(path, `${filenamePrefix}.sbom.json`);
|
|
20
|
-
writeFileSync(outputPath, JSON.stringify(bomJson, null, 2));
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
console.error('Error creating SBOM', error.message);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|