@herodevs/cli 2.0.0-beta.12 → 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 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.12/scripts/install.sh | bash
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.12/scripts/install.sh | bash
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.10 darwin-arm64 node-v22.18.0
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
@@ -82,7 +82,9 @@ 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)
87
+ * [`hd tracker init`](#hd-tracker-init)
86
88
  * [`hd update [CHANNEL]`](#hd-update-channel)
87
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.
88
90
 
@@ -95,7 +97,7 @@ USAGE
95
97
  $ hd help [COMMAND...] [-n]
96
98
 
97
99
  ARGUMENTS
98
- COMMAND... Command to show help for.
100
+ [COMMAND...] Command to show help for.
99
101
 
100
102
  FLAGS
101
103
  -n, --nested-commands Include all nested commands in the output.
@@ -104,7 +106,43 @@ DESCRIPTION
104
106
  Display help for hd.
105
107
  ```
106
108
 
107
- _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.33/src/commands/help.ts)_
109
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.34/src/commands/help.ts)_
110
+
111
+ ## `hd report committers`
112
+
113
+ Generate report of committers to a git repository
114
+
115
+ ```
116
+ USAGE
117
+ $ hd report committers [--json] [-x <value>...] [-d <value>] [--monthly] [-m <value> | -s <value> | -e <value> | | ]
118
+ [-c] [-s]
119
+
120
+ FLAGS
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.
131
+
132
+ DESCRIPTION
133
+ Generate report of committers to a git repository
134
+
135
+ EXAMPLES
136
+ $ hd report committers
137
+
138
+ $ hd report committers --csv -s
139
+
140
+ $ hd report committers --json
141
+
142
+ $ hd report committers --csv
143
+ ```
144
+
145
+ _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/report/committers.ts)_
108
146
 
109
147
  ## `hd scan eol`
110
148
 
@@ -112,18 +150,20 @@ Scan a given SBOM for EOL data
112
150
 
113
151
  ```
114
152
  USAGE
115
- $ hd scan eol [--json] [-f <value> | -d <value>] [-s] [-o <value>] [--saveSbom] [--sbomOutput <value>] [--saveTrimmedSbom] [--hideReportUrl] [--version]
153
+ $ hd scan eol [--json] [-f <value> | -d <value>] [-s] [-o <value>] [--saveSbom] [--sbomOutput <value>]
154
+ [--saveTrimmedSbom] [--hideReportUrl] [--version]
116
155
 
117
156
  FLAGS
118
- -d, --dir=<value> [default: <current directory>] The directory to scan in order to scan for EOL
119
- -f, --file=<value> The file path of an existing SBOM to scan for EOL (supports CycloneDX and SPDX 2.3 formats)
120
- -s, --save Save the generated report as herodevs.report.json in the scanned directory
121
- -o, --output=<value> Save the generated report to a custom path (requires --save, defaults to herodevs.report.json when not provided)
122
- --hideReportUrl Hide the generated web report URL for this scan
123
- --saveSbom Save the generated SBOM as herodevs.sbom.json in the scanned directory
124
- --sbomOutput=<value> Save the generated SBOM to a custom path (requires --saveSbom, defaults to herodevs.sbom.json when not provided)
125
- --saveTrimmedSbom Save the trimmed SBOM as herodevs.sbom-trimmed.json in the scanned directory
126
- --version Show CLI version.
157
+ -d, --dir=<value> [default: <current directory>] The directory to scan in order to create a cyclonedx SBOM
158
+ -f, --file=<value> The file path of an existing SBOM to scan for EOL (supports CycloneDX and SPDX 2.3 formats)
159
+ -o, --output=<value> Save the generated report to a custom path (defaults to herodevs.report.json when not
160
+ provided)
161
+ -s, --save Save the generated report as herodevs.report.json in the scanned directory
162
+ --hideReportUrl Hide the generated web report URL for this scan
163
+ --saveSbom Save the generated SBOM as herodevs.sbom.json in the scanned directory
164
+ --saveTrimmedSbom Save the trimmed SBOM as herodevs.sbom-trimmed.json in the scanned directory
165
+ --sbomOutput=<value> Save the generated SBOM to a custom path (defaults to herodevs.sbom.json when not provided)
166
+ --version Show CLI version.
127
167
 
128
168
  GLOBAL FLAGS
129
169
  --json Format output as json.
@@ -157,7 +197,41 @@ EXAMPLES
157
197
  $ hd scan eol --json
158
198
  ```
159
199
 
160
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.10/src/commands/scan/eol.ts)_
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)_
161
235
 
162
236
  ## `hd update [CHANNEL]`
163
237
 
@@ -197,7 +271,7 @@ EXAMPLES
197
271
  $ hd update --available
198
272
  ```
199
273
 
200
- _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.8/src/commands/update.ts)_
274
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.13/src/commands/update.ts)_
201
275
  <!-- commandsstop -->
202
276
 
203
277
  ## CI/CD Usage
@@ -1,4 +1,4 @@
1
- import { gql } from '@apollo/client/core/core.cjs';
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 {
@@ -1,6 +1,6 @@
1
- import { ApolloClient } from '@apollo/client/core/index.js';
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<import("@apollo/client/core/index.js").NormalizedCacheObject>;
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>;
@@ -1,4 +1,4 @@
1
- import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client/core/index.js';
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
- const res = await client.mutate({
21
+ let res;
22
+ res = await client.mutate({
22
23
  mutation: createReportMutation,
23
24
  variables: { input },
24
25
  });
25
- if (res?.errors?.length) {
26
- debugLogger('GraphQL errors in createReport: %o', res.errors);
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
- const batchResponses = await Promise.all(batch);
51
+ let batchResponses;
52
+ batchResponses = await Promise.all(batch);
51
53
  for (const response of batchResponses) {
52
- if (response?.errors?.length) {
53
- debugLogger('GraphQL errors in getReport query: %o', response.errors);
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;
@@ -0,0 +1,27 @@
1
+ import { Command } from '@oclif/core';
2
+ import { type CommittersReport } 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
+ 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>;
14
+ months: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
15
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ };
18
+ run(): Promise<CommittersReport | string>;
19
+ /**
20
+ * Fetches git commit data with month and author information
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
25
+ */
26
+ private fetchGitCommitData;
27
+ }
@@ -0,0 +1,215 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import { Command, Flags } from '@oclif/core';
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";
8
+ import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
9
+ export default class Committers extends Command {
10
+ static description = 'Generate report of committers to a git repository';
11
+ static enableJsonFlag = true;
12
+ static examples = [
13
+ '<%= config.bin %> <%= command.id %>',
14
+ '<%= config.bin %> <%= command.id %> --csv -s',
15
+ '<%= config.bin %> <%= command.id %> --json',
16
+ '<%= config.bin %> <%= command.id %> --csv',
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
+ }),
47
+ months: Flags.integer({
48
+ char: 'm',
49
+ description: 'The number of months of git history to review. Cannot be used along beforeDate and afterDate',
50
+ default: 12,
51
+ exclusive: ['beforeDate', 'afterDate', 's', 'e'],
52
+ }),
53
+ csv: Flags.boolean({
54
+ char: 'c',
55
+ description: 'Output in CSV format',
56
+ default: false,
57
+ }),
58
+ save: Flags.boolean({
59
+ char: 's',
60
+ description: `Save the committers report as ${filenamePrefix}.committers.<output>`,
61
+ default: false,
62
+ }),
63
+ };
64
+ async run() {
65
+ const { flags } = await this.parse(Committers);
66
+ const { afterDate, beforeDate, exclude, directory: cwd, monthly, months, csv, save } = flags;
67
+ const isJson = this.jsonEnabled();
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;
74
+ try {
75
+ const entries = this.fetchGitCommitData(afterDateStartOfDay, beforeDateEndOfDay, ignores, cwd);
76
+ if (entries.length === 0) {
77
+ return `No commits found between ${afterDate} and ${beforeDate}`;
78
+ }
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
+ });
123
+ }
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
+ });
158
+ }
159
+ break;
160
+ }
161
+ if (save) {
162
+ try {
163
+ fs.writeFileSync(`${filenamePrefix}.${monthly ? 'monthly' : 'committers'}.${reportFormat}`, finalReport, {
164
+ encoding: 'utf-8',
165
+ });
166
+ this.log(`Report written to ${reportFormat.toUpperCase()}`);
167
+ }
168
+ catch (err) {
169
+ this.error(`Failed to save ${reportFormat.toUpperCase()} report: ${getErrorMessage(err)}`);
170
+ }
171
+ }
172
+ this.log(finalReport);
173
+ return finalReport;
174
+ }
175
+ catch (error) {
176
+ this.error(`Failed to generate report: ${getErrorMessage(error)}`);
177
+ }
178
+ }
179
+ /**
180
+ * Fetches git commit data with month and author information
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
185
+ */
186
+ fetchGitCommitData(sinceDate, beforeDateEndOfDay, ignores, cwd) {
187
+ const logParameters = [
188
+ 'log',
189
+ `--since="${sinceDate}"`,
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
+ });
198
+ if (logProcess.error) {
199
+ if (isErrnoException(logProcess.error)) {
200
+ if (logProcess.error.code === 'ENOENT') {
201
+ this.error('Git command not found. Please ensure git is installed and available in your PATH.');
202
+ }
203
+ this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
204
+ }
205
+ this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
206
+ }
207
+ if (logProcess.status !== 0) {
208
+ this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`);
209
+ }
210
+ if (!logProcess.stdout) {
211
+ return [];
212
+ }
213
+ return parseGitLogOutput(logProcess.stdout);
214
+ }
215
+ }
@@ -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;
@@ -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
+ };
@@ -0,0 +1,58 @@
1
+ export type ReportFormat = 'txt' | 'csv' | 'json';
2
+ export type CommitEntry = {
3
+ commitHash: string;
4
+ author: string;
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 = {
19
+ [author: string]: number;
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[];
41
+ /**
42
+ * Parses git log output into structured data
43
+ * @param output - Git log command output
44
+ * @returns Parsed commit entries
45
+ */
46
+ export declare function parseGitLogOutput(output: string): CommitEntry[];
47
+ /**
48
+ * Generates commits author report
49
+ * @param entries - commit entries from git log
50
+ * @returns Commits Author Report
51
+ */
52
+ export declare function generateCommittersReport(entries: CommitEntry[]): AuthorReportRow[];
53
+ /**
54
+ * Generates commits monthly report
55
+ * @param entries - commit entries from git log
56
+ * @returns Monthly Report
57
+ */
58
+ export declare function generateMonthlyReport(entries: CommitEntry[]): MonthlyReportRow[];
@@ -0,0 +1,78 @@
1
+ import { endOfMonth, formatDate, parse } from 'date-fns';
2
+ import { DEFAULT_DATE_COMMIT_MONTH_FORMAT, DEFAULT_DATE_FORMAT } from '../config/constants.js';
3
+ /**
4
+ * Parses git log output into structured data
5
+ * @param output - Git log command output
6
+ * @returns Parsed commit entries
7
+ */
8
+ export function parseGitLogOutput(output) {
9
+ return output
10
+ .split('\n')
11
+ .filter(Boolean)
12
+ .map((line) => {
13
+ // Remove surrounding double quotes if present (e.g. "March|John Doe" → March|John Doe)
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
+ };
21
+ });
22
+ }
23
+ /**
24
+ * Generates commits author report
25
+ * @param entries - commit entries from git log
26
+ * @returns Commits Author Report
27
+ */
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
+ });
38
+ }
39
+ return acc;
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);
47
+ }
48
+ /**
49
+ * Generates commits monthly report
50
+ * @param entries - commit entries from git log
51
+ * @returns Monthly Report
52
+ */
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
+ });
70
+ }
71
+ return acc;
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());
78
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { type TrackerConfig } from '../config/tracker.config.js';
2
+ export declare const getRootDir: (path: string) => string;
3
+ export declare const createTrackerConfig: (rootPath: string, config: TrackerConfig, overwrite?: boolean) => Promise<void>;
@@ -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.12",
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.20",
43
- "@apollo/client": "^3.13.8",
42
+ "@amplitude/analytics-node": "^1.5.22",
43
+ "@apollo/client": "^4.0.9",
44
44
  "@cyclonedx/cdxgen": "^11.11.0",
45
- "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.11",
46
- "@oclif/core": "^4.5.3",
45
+ "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.12",
46
+ "@inquirer/prompts": "^8.0.1",
47
+ "@oclif/core": "^4.8.0",
47
48
  "@oclif/plugin-help": "^6.2.32",
48
- "@oclif/plugin-update": "^4.7.13",
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.2.2",
59
+ "@biomejs/biome": "^2.3.4",
57
60
  "@oclif/test": "^4.1.13",
58
61
  "@types/inquirer": "^9.0.9",
59
- "@types/node": "^24.9.2",
62
+ "@types/mock-fs": "^4.13.4",
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
- "oclif": "^4.22.38",
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",