@herodevs/cli 2.0.0-beta.13 → 2.0.0-beta.15

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.
Files changed (63) hide show
  1. package/README.md +192 -20
  2. package/dist/api/apollo.client.d.ts +3 -0
  3. package/dist/api/apollo.client.js +53 -0
  4. package/dist/api/ci-token.client.d.ts +26 -0
  5. package/dist/api/ci-token.client.js +95 -0
  6. package/dist/api/errors.d.ts +8 -0
  7. package/dist/api/errors.js +13 -0
  8. package/dist/api/gql-operations.d.ts +3 -0
  9. package/dist/api/gql-operations.js +36 -1
  10. package/dist/api/graphql-errors.d.ts +6 -0
  11. package/dist/api/graphql-errors.js +22 -0
  12. package/dist/api/nes.client.d.ts +1 -2
  13. package/dist/api/nes.client.js +31 -20
  14. package/dist/api/user-setup.client.d.ts +15 -0
  15. package/dist/api/user-setup.client.js +92 -0
  16. package/dist/commands/auth/login.d.ts +14 -0
  17. package/dist/commands/auth/login.js +225 -0
  18. package/dist/commands/auth/logout.d.ts +5 -0
  19. package/dist/commands/auth/logout.js +27 -0
  20. package/dist/commands/auth/provision-ci-token.d.ts +5 -0
  21. package/dist/commands/auth/provision-ci-token.js +62 -0
  22. package/dist/commands/report/committers.d.ts +11 -7
  23. package/dist/commands/report/committers.js +144 -76
  24. package/dist/commands/scan/eol.d.ts +2 -0
  25. package/dist/commands/scan/eol.js +34 -4
  26. package/dist/commands/tracker/init.d.ts +14 -0
  27. package/dist/commands/tracker/init.js +84 -0
  28. package/dist/commands/tracker/run.d.ts +15 -0
  29. package/dist/commands/tracker/run.js +183 -0
  30. package/dist/config/constants.d.ts +14 -0
  31. package/dist/config/constants.js +15 -0
  32. package/dist/config/tracker.config.d.ts +16 -0
  33. package/dist/config/tracker.config.js +16 -0
  34. package/dist/hooks/finally/finally.js +10 -4
  35. package/dist/hooks/init/01_initialize_amplitude.js +20 -9
  36. package/dist/service/analytics.svc.d.ts +10 -3
  37. package/dist/service/analytics.svc.js +180 -18
  38. package/dist/service/auth-config.svc.d.ts +5 -0
  39. package/dist/service/auth-config.svc.js +20 -0
  40. package/dist/service/auth-refresh.svc.d.ts +8 -0
  41. package/dist/service/auth-refresh.svc.js +45 -0
  42. package/dist/service/auth-token.svc.d.ts +11 -0
  43. package/dist/service/auth-token.svc.js +48 -0
  44. package/dist/service/auth.svc.d.ts +27 -0
  45. package/dist/service/auth.svc.js +88 -0
  46. package/dist/service/ci-auth.svc.d.ts +6 -0
  47. package/dist/service/ci-auth.svc.js +32 -0
  48. package/dist/service/ci-token.svc.d.ts +6 -0
  49. package/dist/service/ci-token.svc.js +75 -0
  50. package/dist/service/committers.svc.d.ts +46 -58
  51. package/dist/service/committers.svc.js +55 -173
  52. package/dist/service/jwt.svc.d.ts +1 -0
  53. package/dist/service/jwt.svc.js +19 -0
  54. package/dist/service/tracker.svc.d.ts +58 -0
  55. package/dist/service/tracker.svc.js +101 -0
  56. package/dist/types/auth.d.ts +9 -0
  57. package/dist/types/auth.js +1 -0
  58. package/dist/utils/open-in-browser.d.ts +1 -0
  59. package/dist/utils/open-in-browser.js +21 -0
  60. package/dist/utils/retry.d.ts +11 -0
  61. package/dist/utils/retry.js +29 -0
  62. package/dist/utils/strip-typename.js +2 -1
  63. package/package.json +38 -19
@@ -0,0 +1,11 @@
1
+ export interface StoredTokens {
2
+ accessToken?: string;
3
+ refreshToken?: string;
4
+ }
5
+ export declare function saveTokens(tokens: {
6
+ accessToken: string;
7
+ refreshToken?: string;
8
+ }): Promise<[void, unknown]>;
9
+ export declare function getStoredTokens(): Promise<StoredTokens | undefined>;
10
+ export declare function clearStoredTokens(): Promise<[unknown, unknown]>;
11
+ export declare function isAccessTokenExpired(token: string | undefined): boolean;
@@ -0,0 +1,48 @@
1
+ import { AsyncEntry } from '@napi-rs/keyring';
2
+ import { getAccessTokenKey, getRefreshTokenKey, getTokenServiceName } from "./auth-config.svc.js";
3
+ import { decodeJwtPayload } from "./jwt.svc.js";
4
+ const TOKEN_SKEW_SECONDS = 30;
5
+ export async function saveTokens(tokens) {
6
+ const service = getTokenServiceName();
7
+ const accessKey = getAccessTokenKey();
8
+ const refreshKey = getRefreshTokenKey();
9
+ const accessTokenSet = new AsyncEntry(service, accessKey).setPassword(tokens.accessToken);
10
+ const refreshTokenSet = tokens.refreshToken
11
+ ? new AsyncEntry(service, refreshKey).setPassword(tokens.refreshToken)
12
+ : new AsyncEntry(service, refreshKey).deletePassword();
13
+ return Promise.all([accessTokenSet, refreshTokenSet]);
14
+ }
15
+ export async function getStoredTokens() {
16
+ const service = getTokenServiceName();
17
+ const accessKey = getAccessTokenKey();
18
+ const refreshKey = getRefreshTokenKey();
19
+ return Promise.all([
20
+ new AsyncEntry(service, accessKey).getPassword(),
21
+ new AsyncEntry(service, refreshKey).getPassword(),
22
+ ]).then(([accessToken, refreshToken]) => {
23
+ if (!accessToken && !refreshToken) {
24
+ return;
25
+ }
26
+ return {
27
+ accessToken,
28
+ refreshToken,
29
+ };
30
+ });
31
+ }
32
+ export async function clearStoredTokens() {
33
+ const service = getTokenServiceName();
34
+ const accessKey = getAccessTokenKey();
35
+ const refreshKey = getRefreshTokenKey();
36
+ return Promise.all([
37
+ new AsyncEntry(service, accessKey).deletePassword(),
38
+ new AsyncEntry(service, refreshKey).deletePassword(),
39
+ ]);
40
+ }
41
+ export function isAccessTokenExpired(token) {
42
+ const payload = decodeJwtPayload(token);
43
+ if (!payload?.exp) {
44
+ return true;
45
+ }
46
+ const now = Date.now() / 1000;
47
+ return now + TOKEN_SKEW_SECONDS >= payload.exp;
48
+ }
@@ -0,0 +1,27 @@
1
+ import type { TokenResponse } from '../types/auth.ts';
2
+ export type { CITokenErrorCode } from './ci-auth.svc.ts';
3
+ export { CITokenError } from './ci-auth.svc.ts';
4
+ export type AuthErrorCode = 'NOT_LOGGED_IN' | 'SESSION_EXPIRED';
5
+ export type TokenSource = 'oauth' | 'ci';
6
+ export type TokenProvider = (forceRefresh?: boolean) => Promise<string>;
7
+ export declare const AUTH_ERROR_MESSAGES: {
8
+ readonly UNAUTHENTICATED: "Please log in to perform this action. To authenticate, please run an \"auth login\" command.";
9
+ readonly SESSION_EXPIRED: "Your session has expired. To re-authenticate, please run an \"auth login\" command.";
10
+ readonly INVALID_TOKEN: "Your session has expired. To re-authenticate, please run an \"auth login\" command.";
11
+ readonly FORBIDDEN: "You do not have permission to perform this action.";
12
+ readonly NOT_LOGGED_IN_GENERIC: "You are not logged in. Please run an \"auth login\" command to authenticate.";
13
+ };
14
+ export declare function getTokenForScanWithSource(preferOAuth?: boolean): Promise<{
15
+ token: string;
16
+ source: TokenSource;
17
+ }>;
18
+ export declare class AuthError extends Error {
19
+ readonly code: AuthErrorCode;
20
+ constructor(message: string, code: AuthErrorCode);
21
+ }
22
+ export declare function persistTokenResponse(token: TokenResponse): Promise<void>;
23
+ export declare function getAccessToken(): Promise<string | undefined>;
24
+ export declare function getTokenProvider(preferOAuth?: boolean): TokenProvider;
25
+ export declare function requireAccessToken(): Promise<string>;
26
+ export declare function logoutLocally(): Promise<void>;
27
+ export declare const requireAccessTokenForScan: TokenProvider;
@@ -0,0 +1,88 @@
1
+ import { refreshTokens } from "./auth-refresh.svc.js";
2
+ import { clearStoredTokens, getStoredTokens, isAccessTokenExpired, saveTokens } from "./auth-token.svc.js";
3
+ import { requireCIAccessToken } from "./ci-auth.svc.js";
4
+ import { getCIToken } from "./ci-token.svc.js";
5
+ import { debugLogger } from "./log.svc.js";
6
+ export { CITokenError } from "./ci-auth.svc.js";
7
+ export const AUTH_ERROR_MESSAGES = {
8
+ UNAUTHENTICATED: 'Please log in to perform this action. To authenticate, please run an "auth login" command.',
9
+ SESSION_EXPIRED: 'Your session has expired. To re-authenticate, please run an "auth login" command.',
10
+ INVALID_TOKEN: 'Your session has expired. To re-authenticate, please run an "auth login" command.',
11
+ FORBIDDEN: 'You do not have permission to perform this action.',
12
+ NOT_LOGGED_IN_GENERIC: 'You are not logged in. Please run an "auth login" command to authenticate.',
13
+ };
14
+ export async function getTokenForScanWithSource(preferOAuth) {
15
+ if (preferOAuth) {
16
+ const token = await requireAccessToken();
17
+ return { token, source: 'oauth' };
18
+ }
19
+ const tokens = await getStoredTokens();
20
+ if (tokens?.accessToken && !isAccessTokenExpired(tokens.accessToken)) {
21
+ return { token: tokens.accessToken, source: 'oauth' };
22
+ }
23
+ if (tokens?.refreshToken) {
24
+ try {
25
+ const newTokens = await refreshTokens(tokens.refreshToken);
26
+ await persistTokenResponse(newTokens);
27
+ return { token: newTokens.access_token, source: 'oauth' };
28
+ }
29
+ catch (error) {
30
+ debugLogger('Token refresh failed: %O', error);
31
+ }
32
+ }
33
+ const ciToken = getCIToken();
34
+ if (ciToken) {
35
+ const accessToken = await requireCIAccessToken();
36
+ return { token: accessToken, source: 'ci' };
37
+ }
38
+ if (!tokens?.accessToken) {
39
+ throw new AuthError(AUTH_ERROR_MESSAGES.UNAUTHENTICATED, 'NOT_LOGGED_IN');
40
+ }
41
+ throw new AuthError(AUTH_ERROR_MESSAGES.SESSION_EXPIRED, 'SESSION_EXPIRED');
42
+ }
43
+ export class AuthError extends Error {
44
+ code;
45
+ constructor(message, code) {
46
+ super(message);
47
+ this.name = 'AuthError';
48
+ this.code = code;
49
+ }
50
+ }
51
+ export async function persistTokenResponse(token) {
52
+ await saveTokens({
53
+ accessToken: token.access_token,
54
+ refreshToken: token.refresh_token,
55
+ });
56
+ }
57
+ export async function getAccessToken() {
58
+ const tokens = await getStoredTokens();
59
+ if (!tokens) {
60
+ return;
61
+ }
62
+ if (tokens.accessToken && !isAccessTokenExpired(tokens.accessToken)) {
63
+ return tokens.accessToken;
64
+ }
65
+ if (!tokens.refreshToken) {
66
+ return;
67
+ }
68
+ const refreshed = await refreshTokens(tokens.refreshToken);
69
+ await persistTokenResponse(refreshed);
70
+ return refreshed.access_token;
71
+ }
72
+ export function getTokenProvider(preferOAuth) {
73
+ return async (_forceRefresh) => {
74
+ const { token } = await getTokenForScanWithSource(preferOAuth);
75
+ return token;
76
+ };
77
+ }
78
+ export async function requireAccessToken() {
79
+ const token = await getAccessToken();
80
+ if (!token) {
81
+ throw new Error(AUTH_ERROR_MESSAGES.NOT_LOGGED_IN_GENERIC);
82
+ }
83
+ return token;
84
+ }
85
+ export async function logoutLocally() {
86
+ await clearStoredTokens();
87
+ }
88
+ export const requireAccessTokenForScan = getTokenProvider();
@@ -0,0 +1,6 @@
1
+ export type CITokenErrorCode = 'CI_TOKEN_INVALID' | 'CI_TOKEN_REFRESH_FAILED' | 'CI_ORG_ID_REQUIRED';
2
+ export declare class CITokenError extends Error {
3
+ readonly code: CITokenErrorCode;
4
+ constructor(message: string, code: CITokenErrorCode);
5
+ }
6
+ export declare function requireCIAccessToken(): Promise<string>;
@@ -0,0 +1,32 @@
1
+ import { exchangeCITokenForAccess } from "../api/ci-token.client.js";
2
+ import { config } from "../config/constants.js";
3
+ import { getCIToken, saveCIToken } from "./ci-token.svc.js";
4
+ import { debugLogger } from "./log.svc.js";
5
+ const CITOKEN_ERROR_MESSAGE = "CI token is invalid or expired. To provision a new CI token, run 'hd auth provision-ci-token' (after logging in with 'hd auth login').";
6
+ export class CITokenError extends Error {
7
+ code;
8
+ constructor(message, code) {
9
+ super(message);
10
+ this.name = 'CITokenError';
11
+ this.code = code;
12
+ }
13
+ }
14
+ export async function requireCIAccessToken() {
15
+ const ciToken = getCIToken();
16
+ if (!ciToken) {
17
+ throw new CITokenError(CITOKEN_ERROR_MESSAGE, 'CI_TOKEN_INVALID');
18
+ }
19
+ try {
20
+ const result = await exchangeCITokenForAccess({
21
+ refreshToken: ciToken,
22
+ });
23
+ if (result.refreshToken && config.ciTokenFromEnv === undefined) {
24
+ saveCIToken(result.refreshToken);
25
+ }
26
+ return result.accessToken;
27
+ }
28
+ catch (error) {
29
+ debugLogger('CI token refresh failed: %O', error);
30
+ throw new CITokenError(CITOKEN_ERROR_MESSAGE, 'CI_TOKEN_REFRESH_FAILED');
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ export declare function encryptToken(plaintext: string): string;
2
+ export declare function decryptToken(encoded: string): string;
3
+ export declare function getCITokenFromStorage(): string | undefined;
4
+ export declare function getCIToken(): string | undefined;
5
+ export declare function saveCIToken(token: string): void;
6
+ export declare function clearCIToken(): void;
@@ -0,0 +1,75 @@
1
+ import crypto from 'node:crypto';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import Conf from 'conf';
5
+ import { config } from "../config/constants.js";
6
+ const CI_TOKEN_STORAGE_KEY = 'ciRefreshToken';
7
+ const ENCRYPTION_SALT = 'hdcli-ci-token-v1';
8
+ const ALGORITHM = 'aes-256-gcm';
9
+ const IV_LENGTH = 12;
10
+ const AUTH_TAG_LENGTH = 16;
11
+ function getConfStore() {
12
+ const cwd = path.join(os.homedir(), '.hdcli');
13
+ return new Conf({
14
+ projectName: 'hdcli',
15
+ cwd,
16
+ configName: 'ci-token',
17
+ });
18
+ }
19
+ function getMachineKey() {
20
+ const hostname = os.hostname();
21
+ const username = os.userInfo().username;
22
+ const raw = `${hostname}:${username}:${ENCRYPTION_SALT}`;
23
+ return crypto.createHash('sha256').update(raw, 'utf8').digest();
24
+ }
25
+ export function encryptToken(plaintext) {
26
+ const key = getMachineKey();
27
+ const iv = crypto.randomBytes(IV_LENGTH);
28
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
29
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
30
+ const authTag = cipher.getAuthTag();
31
+ const combined = Buffer.concat([iv, authTag, encrypted]);
32
+ return combined.toString('base64url');
33
+ }
34
+ export function decryptToken(encoded) {
35
+ const key = getMachineKey();
36
+ const combined = Buffer.from(encoded, 'base64url');
37
+ if (combined.length < IV_LENGTH + AUTH_TAG_LENGTH) {
38
+ throw new Error('Invalid encrypted token format');
39
+ }
40
+ const iv = combined.subarray(0, IV_LENGTH);
41
+ const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
42
+ const ciphertext = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
43
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
44
+ decipher.setAuthTag(authTag);
45
+ return decipher.update(ciphertext).toString('utf8') + decipher.final('utf8');
46
+ }
47
+ export function getCITokenFromStorage() {
48
+ const store = getConfStore();
49
+ const encoded = store.get(CI_TOKEN_STORAGE_KEY);
50
+ if (encoded === undefined || typeof encoded !== 'string') {
51
+ return undefined;
52
+ }
53
+ try {
54
+ return decryptToken(encoded);
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ export function getCIToken() {
61
+ const fromEnv = config.ciTokenFromEnv;
62
+ if (fromEnv !== undefined) {
63
+ return fromEnv;
64
+ }
65
+ return getCITokenFromStorage();
66
+ }
67
+ export function saveCIToken(token) {
68
+ const store = getConfStore();
69
+ const encoded = encryptToken(token);
70
+ store.set(CI_TOKEN_STORAGE_KEY, encoded);
71
+ }
72
+ export function clearCIToken() {
73
+ const store = getConfStore();
74
+ store.delete(CI_TOKEN_STORAGE_KEY);
75
+ }
@@ -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 @@
1
+ export declare function decodeJwtPayload(token: string | undefined): Record<string, unknown> | undefined;