@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 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.13/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.13/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.12 darwin-arm64 node-v24.10.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
@@ -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] [-m <value>] [-c] [-s]
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 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.
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.12/src/commands/report/committers.ts)_
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.12/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)_
195
235
 
196
236
  ## `hd update [CHANNEL]`
197
237
 
@@ -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;
@@ -1,23 +1,27 @@
1
1
  import { Command } from '@oclif/core';
2
- import { type ReportData } from '../../service/committers.svc.ts';
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<ReportData | string>;
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 { filenamePrefix } from "../../config/constants.js";
6
- import { calculateOverallStats, formatAsCsv, formatAsText, groupCommitsByMonth, parseGitLogOutput, } from "../../service/committers.svc.js";
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 sinceDate = `${months} months ago`;
39
- this.log('Starting committers report with flags: %O', flags);
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
- // 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;
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
- 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');
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
- catch (error) {
69
- this.error(`Failed to save CSV report: ${getErrorMessage(error)}`);
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(path.resolve(`${filenamePrefix}.committers.txt`), textOutput);
80
- this.log('Report written to txt');
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 (error) {
83
- this.error(`Failed to save txt report: ${getErrorMessage(error)}`);
168
+ catch (err) {
169
+ this.error(`Failed to save ${reportFormat.toUpperCase()} report: ${getErrorMessage(err)}`);
84
170
  }
85
171
  }
86
- else {
87
- this.log(textOutput);
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 logProcess = spawnSync('git', [
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
- ], { encoding: 'utf-8' });
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;
@@ -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 interface CommitEntry {
2
- month: string;
1
+ export type ReportFormat = 'txt' | 'csv' | 'json';
2
+ export type CommitEntry = {
3
+ commitHash: string;
3
4
  author: string;
4
- }
5
- export interface AuthorCommitCounts {
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 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
- }
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
- * 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
48
+ * Generates commits author report
49
+ * @param entries - commit entries from git log
50
+ * @returns Commits Author Report
62
51
  */
63
- export declare function formatAsText(data: ReportData): string;
52
+ export declare function generateCommittersReport(entries: CommitEntry[]): AuthorReportRow[];
64
53
  /**
65
- * Format output based on user preference
66
- * @param output
67
- * @param reportData
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 formatOutputBasedOnFlag(output: string, reportData: ReportData): string;
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 [month, author] = line.replace(/^"(.*)"$/, '$1').split('|');
13
- return { month, author };
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
- * Groups commit data by month
18
- * @param entries - Commit entries
19
- * @returns Object with months as keys and author commit counts as values
24
+ * Generates commits author report
25
+ * @param entries - commit entries from git log
26
+ * @returns Commits Author Report
20
27
  */
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
+ 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
- // 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;
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
- * Calculates overall commit statistics by author
57
- * @param entries - Commit entries
58
- * @returns Object with authors as keys and total commit counts as values
49
+ * Generates commits monthly report
50
+ * @param entries - commit entries from git log
51
+ * @returns Monthly Report
59
52
  */
60
- export function calculateOverallStats(entries) {
61
- const commitsByAuthor = entries.reduce((acc, entry) => {
62
- const authorKey = entry.author;
63
- if (!acc[authorKey]) {
64
- acc[authorKey] = [];
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
- 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;
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,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.13",
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.21",
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
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.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.3.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
- "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",
@@ -110,4 +115,4 @@
110
115
  }
111
116
  },
112
117
  "types": "dist/index.d.ts"
113
- }
118
+ }