@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.
- package/README.md +192 -20
- package/dist/api/apollo.client.d.ts +3 -0
- package/dist/api/apollo.client.js +53 -0
- package/dist/api/ci-token.client.d.ts +26 -0
- package/dist/api/ci-token.client.js +95 -0
- package/dist/api/errors.d.ts +8 -0
- package/dist/api/errors.js +13 -0
- package/dist/api/gql-operations.d.ts +3 -0
- package/dist/api/gql-operations.js +36 -1
- package/dist/api/graphql-errors.d.ts +6 -0
- package/dist/api/graphql-errors.js +22 -0
- package/dist/api/nes.client.d.ts +1 -2
- package/dist/api/nes.client.js +31 -20
- package/dist/api/user-setup.client.d.ts +15 -0
- package/dist/api/user-setup.client.js +92 -0
- package/dist/commands/auth/login.d.ts +14 -0
- package/dist/commands/auth/login.js +225 -0
- package/dist/commands/auth/logout.d.ts +5 -0
- package/dist/commands/auth/logout.js +27 -0
- package/dist/commands/auth/provision-ci-token.d.ts +5 -0
- package/dist/commands/auth/provision-ci-token.js +62 -0
- package/dist/commands/report/committers.d.ts +11 -7
- package/dist/commands/report/committers.js +144 -76
- package/dist/commands/scan/eol.d.ts +2 -0
- package/dist/commands/scan/eol.js +34 -4
- package/dist/commands/tracker/init.d.ts +14 -0
- package/dist/commands/tracker/init.js +84 -0
- package/dist/commands/tracker/run.d.ts +15 -0
- package/dist/commands/tracker/run.js +183 -0
- package/dist/config/constants.d.ts +14 -0
- package/dist/config/constants.js +15 -0
- package/dist/config/tracker.config.d.ts +16 -0
- package/dist/config/tracker.config.js +16 -0
- package/dist/hooks/finally/finally.js +10 -4
- package/dist/hooks/init/01_initialize_amplitude.js +20 -9
- package/dist/service/analytics.svc.d.ts +10 -3
- package/dist/service/analytics.svc.js +180 -18
- package/dist/service/auth-config.svc.d.ts +5 -0
- package/dist/service/auth-config.svc.js +20 -0
- package/dist/service/auth-refresh.svc.d.ts +8 -0
- package/dist/service/auth-refresh.svc.js +45 -0
- package/dist/service/auth-token.svc.d.ts +11 -0
- package/dist/service/auth-token.svc.js +48 -0
- package/dist/service/auth.svc.d.ts +27 -0
- package/dist/service/auth.svc.js +88 -0
- package/dist/service/ci-auth.svc.d.ts +6 -0
- package/dist/service/ci-auth.svc.js +32 -0
- package/dist/service/ci-token.svc.d.ts +6 -0
- package/dist/service/ci-token.svc.js +75 -0
- package/dist/service/committers.svc.d.ts +46 -58
- package/dist/service/committers.svc.js +55 -173
- package/dist/service/jwt.svc.d.ts +1 -0
- package/dist/service/jwt.svc.js +19 -0
- package/dist/service/tracker.svc.d.ts +58 -0
- package/dist/service/tracker.svc.js +101 -0
- package/dist/types/auth.d.ts +9 -0
- package/dist/types/auth.js +1 -0
- package/dist/utils/open-in-browser.d.ts +1 -0
- package/dist/utils/open-in-browser.js +21 -0
- package/dist/utils/retry.d.ts +11 -0
- package/dist/utils/retry.js +29 -0
- package/dist/utils/strip-typename.js +2 -1
- 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
|
|
2
|
-
|
|
1
|
+
export type ReportFormat = 'txt' | 'csv' | 'json';
|
|
2
|
+
export type CommitEntry = {
|
|
3
|
+
commitHash: string;
|
|
3
4
|
author: string;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
date: Date;
|
|
6
|
+
monthGroup: string;
|
|
7
|
+
};
|
|
8
|
+
export type CommitAuthorData = {
|
|
9
|
+
commits: CommitEntry[];
|
|
10
|
+
lastCommitOn: Date;
|
|
11
|
+
};
|
|
12
|
+
export type CommitMonthData = {
|
|
13
|
+
start: Date | string;
|
|
14
|
+
end: Date | string;
|
|
15
|
+
totalCommits: number;
|
|
16
|
+
committers: AuthorCommitCount;
|
|
17
|
+
};
|
|
18
|
+
export type AuthorCommitCount = {
|
|
6
19
|
[author: string]: number;
|
|
7
|
-
}
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
};
|
|
21
|
+
export type AuthorReportTableRow = {
|
|
22
|
+
index: number;
|
|
23
|
+
author: string;
|
|
24
|
+
commits: number;
|
|
25
|
+
lastCommitOn: string;
|
|
26
|
+
};
|
|
27
|
+
export type MonthlyReportTableRow = {
|
|
28
|
+
index: number;
|
|
29
|
+
month: number;
|
|
30
|
+
start: string;
|
|
31
|
+
end: string;
|
|
32
|
+
totalCommits: number;
|
|
33
|
+
};
|
|
34
|
+
export type MonthlyReportRow = {
|
|
35
|
+
month: string;
|
|
36
|
+
} & CommitMonthData;
|
|
37
|
+
export type AuthorReportRow = {
|
|
38
|
+
author: string;
|
|
39
|
+
} & CommitAuthorData;
|
|
40
|
+
export type CommittersReport = AuthorReportRow[] | MonthlyReportRow[];
|
|
23
41
|
/**
|
|
24
42
|
* Parses git log output into structured data
|
|
25
43
|
* @param output - Git log command output
|
|
@@ -27,44 +45,14 @@ export interface ReportData {
|
|
|
27
45
|
*/
|
|
28
46
|
export declare function parseGitLogOutput(output: string): CommitEntry[];
|
|
29
47
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param entries -
|
|
32
|
-
* @returns
|
|
33
|
-
*/
|
|
34
|
-
export declare function groupCommitsByMonth(entries: CommitEntry[]): MonthlyData;
|
|
35
|
-
/**
|
|
36
|
-
* Calculates overall commit statistics by author
|
|
37
|
-
* @param entries - Commit entries
|
|
38
|
-
* @returns Object with authors as keys and total commit counts as values
|
|
39
|
-
*/
|
|
40
|
-
export declare function calculateOverallStats(entries: CommitEntry[]): AuthorCommitCounts;
|
|
41
|
-
/**
|
|
42
|
-
* Formats monthly report sections
|
|
43
|
-
* @param monthlyData - Grouped commit data by month
|
|
44
|
-
* @returns Formatted monthly report sections
|
|
45
|
-
*/
|
|
46
|
-
export declare function formatMonthlyReport(monthlyData: MonthlyData): string;
|
|
47
|
-
/**
|
|
48
|
-
* Formats overall statistics section
|
|
49
|
-
* @param overallStats - Overall commit counts by author
|
|
50
|
-
* @param grandTotal - Total number of commits
|
|
51
|
-
* @returns Formatted overall statistics section
|
|
52
|
-
*/
|
|
53
|
-
export declare function formatOverallStats(overallStats: AuthorCommitCounts, grandTotal: number): string;
|
|
54
|
-
/**
|
|
55
|
-
* Formats the report data as CSV
|
|
56
|
-
* @param data - The structured report data
|
|
57
|
-
*/
|
|
58
|
-
export declare function formatAsCsv(data: ReportData): string;
|
|
59
|
-
/**
|
|
60
|
-
* Formats the report data as text
|
|
61
|
-
* @param data - The structured report data
|
|
48
|
+
* Generates commits author report
|
|
49
|
+
* @param entries - commit entries from git log
|
|
50
|
+
* @returns Commits Author Report
|
|
62
51
|
*/
|
|
63
|
-
export declare function
|
|
52
|
+
export declare function generateCommittersReport(entries: CommitEntry[]): AuthorReportRow[];
|
|
64
53
|
/**
|
|
65
|
-
*
|
|
66
|
-
* @param
|
|
67
|
-
* @
|
|
68
|
-
* @returns
|
|
54
|
+
* Generates commits monthly report
|
|
55
|
+
* @param entries - commit entries from git log
|
|
56
|
+
* @returns Monthly Report
|
|
69
57
|
*/
|
|
70
|
-
export declare function
|
|
58
|
+
export declare function generateMonthlyReport(entries: CommitEntry[]): MonthlyReportRow[];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { endOfMonth, formatDate, parse } from 'date-fns';
|
|
2
|
+
import { DEFAULT_DATE_COMMIT_MONTH_FORMAT, DEFAULT_DATE_FORMAT } from '../config/constants.js';
|
|
1
3
|
/**
|
|
2
4
|
* Parses git log output into structured data
|
|
3
5
|
* @param output - Git log command output
|
|
@@ -9,188 +11,68 @@ export function parseGitLogOutput(output) {
|
|
|
9
11
|
.filter(Boolean)
|
|
10
12
|
.map((line) => {
|
|
11
13
|
// Remove surrounding double quotes if present (e.g. "March|John Doe" → March|John Doe)
|
|
12
|
-
const [
|
|
13
|
-
return {
|
|
14
|
+
const [commitHash, author, date] = line.replace(/^"(.*)"$/, '$1').split('|');
|
|
15
|
+
return {
|
|
16
|
+
commitHash,
|
|
17
|
+
author,
|
|
18
|
+
date: parse(formatDate(new Date(date), DEFAULT_DATE_FORMAT), DEFAULT_DATE_FORMAT, new Date()),
|
|
19
|
+
monthGroup: formatDate(new Date(date), DEFAULT_DATE_COMMIT_MONTH_FORMAT),
|
|
20
|
+
};
|
|
14
21
|
});
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param entries -
|
|
19
|
-
* @returns
|
|
24
|
+
* Generates commits author report
|
|
25
|
+
* @param entries - commit entries from git log
|
|
26
|
+
* @returns Commits Author Report
|
|
20
27
|
*/
|
|
21
|
-
export function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
acc
|
|
28
|
+
export function generateCommittersReport(entries) {
|
|
29
|
+
return Array.from(entries
|
|
30
|
+
.sort((a, b) => b.date.valueOf() - a.date.valueOf())
|
|
31
|
+
.reduce((acc, curr, _index, array) => {
|
|
32
|
+
if (!acc.has(curr.author)) {
|
|
33
|
+
const byAuthor = array.filter((c) => c.author === curr.author);
|
|
34
|
+
acc.set(curr.author, {
|
|
35
|
+
commits: byAuthor,
|
|
36
|
+
lastCommitOn: byAuthor[0].date,
|
|
37
|
+
});
|
|
28
38
|
}
|
|
29
|
-
acc[monthKey].push(entry);
|
|
30
39
|
return acc;
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Count commits per author for this month
|
|
39
|
-
const commitsByAuthor = commits.reduce((acc, entry) => {
|
|
40
|
-
const authorKey = entry.author;
|
|
41
|
-
if (!acc[authorKey]) {
|
|
42
|
-
acc[authorKey] = [];
|
|
43
|
-
}
|
|
44
|
-
acc[authorKey].push(entry);
|
|
45
|
-
return acc;
|
|
46
|
-
}, {});
|
|
47
|
-
const authorCounts = {};
|
|
48
|
-
for (const [author, authorCommits] of Object.entries(commitsByAuthor)) {
|
|
49
|
-
authorCounts[author] = authorCommits?.length ?? 0;
|
|
50
|
-
}
|
|
51
|
-
result[month] = authorCounts;
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
40
|
+
}, new Map()))
|
|
41
|
+
.map(([key, value]) => ({
|
|
42
|
+
author: key,
|
|
43
|
+
commits: value.commits,
|
|
44
|
+
lastCommitOn: value.lastCommitOn,
|
|
45
|
+
}))
|
|
46
|
+
.sort((a, b) => b.commits.length - a.commits.length);
|
|
54
47
|
}
|
|
55
48
|
/**
|
|
56
|
-
*
|
|
57
|
-
* @param entries -
|
|
58
|
-
* @returns
|
|
49
|
+
* Generates commits monthly report
|
|
50
|
+
* @param entries - commit entries from git log
|
|
51
|
+
* @returns Monthly Report
|
|
59
52
|
*/
|
|
60
|
-
export function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
export function generateMonthlyReport(entries) {
|
|
54
|
+
return Array.from(entries
|
|
55
|
+
.sort((a, b) => b.date.valueOf() - a.date.valueOf())
|
|
56
|
+
.reduce((acc, curr, _index, array) => {
|
|
57
|
+
if (!acc.has(curr.monthGroup)) {
|
|
58
|
+
const monthlyCommits = array.filter((e) => e.monthGroup === curr.monthGroup);
|
|
59
|
+
acc.set(curr.monthGroup, {
|
|
60
|
+
start: formatDate(monthlyCommits[0].date, DEFAULT_DATE_FORMAT),
|
|
61
|
+
end: formatDate(endOfMonth(monthlyCommits[0].date), DEFAULT_DATE_FORMAT),
|
|
62
|
+
totalCommits: monthlyCommits.length,
|
|
63
|
+
committers: monthlyCommits.reduce((acc, curr) => {
|
|
64
|
+
if (!acc[curr.author]) {
|
|
65
|
+
acc[curr.author] = monthlyCommits.filter((c) => c.author === curr.author).length;
|
|
66
|
+
}
|
|
67
|
+
return acc;
|
|
68
|
+
}, {}),
|
|
69
|
+
});
|
|
65
70
|
}
|
|
66
|
-
acc[authorKey].push(entry);
|
|
67
71
|
return acc;
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Formats monthly report sections
|
|
78
|
-
* @param monthlyData - Grouped commit data by month
|
|
79
|
-
* @returns Formatted monthly report sections
|
|
80
|
-
*/
|
|
81
|
-
export function formatMonthlyReport(monthlyData) {
|
|
82
|
-
const sortedMonths = Object.keys(monthlyData).sort();
|
|
83
|
-
let report = '';
|
|
84
|
-
for (const month of sortedMonths) {
|
|
85
|
-
report += `\n## ${month}\n`;
|
|
86
|
-
const authors = Object.entries(monthlyData[month]).sort((a, b) => b[1] - a[1]);
|
|
87
|
-
for (const [author, count] of authors) {
|
|
88
|
-
report += `${count.toString().padStart(6)} ${author}\n`;
|
|
89
|
-
}
|
|
90
|
-
const monthTotal = authors.reduce((sum, [_, count]) => sum + count, 0);
|
|
91
|
-
report += `${monthTotal.toString().padStart(6)} TOTAL\n`;
|
|
92
|
-
}
|
|
93
|
-
return report;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Formats overall statistics section
|
|
97
|
-
* @param overallStats - Overall commit counts by author
|
|
98
|
-
* @param grandTotal - Total number of commits
|
|
99
|
-
* @returns Formatted overall statistics section
|
|
100
|
-
*/
|
|
101
|
-
export function formatOverallStats(overallStats, grandTotal) {
|
|
102
|
-
let report = '\n## Overall Statistics\n';
|
|
103
|
-
const sortedStats = Object.entries(overallStats).sort((a, b) => b[1] - a[1]);
|
|
104
|
-
for (const [author, count] of sortedStats) {
|
|
105
|
-
report += `${count.toString().padStart(6)} ${author}\n`;
|
|
106
|
-
}
|
|
107
|
-
report += `${grandTotal.toString().padStart(6)} GRAND TOTAL\n`;
|
|
108
|
-
return report;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Formats the report data as CSV
|
|
112
|
-
* @param data - The structured report data
|
|
113
|
-
*/
|
|
114
|
-
export function formatAsCsv(data) {
|
|
115
|
-
// First prepare all author names (for columns)
|
|
116
|
-
const allAuthors = new Set();
|
|
117
|
-
// Collect all unique author names
|
|
118
|
-
for (const monthData of Object.values(data.monthly)) {
|
|
119
|
-
for (const author of Object.keys(monthData)) {
|
|
120
|
-
if (author !== 'total')
|
|
121
|
-
allAuthors.add(author);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
const authors = Array.from(allAuthors).sort();
|
|
125
|
-
// Create CSV header
|
|
126
|
-
let csv = `Month,${authors.join(',')},Total\n`;
|
|
127
|
-
// Add monthly data rows
|
|
128
|
-
const sortedMonths = Object.keys(data.monthly).sort();
|
|
129
|
-
for (const month of sortedMonths) {
|
|
130
|
-
csv += month;
|
|
131
|
-
// Add data for each author
|
|
132
|
-
for (const author of authors) {
|
|
133
|
-
const count = data.monthly[month][author] || 0;
|
|
134
|
-
csv += `,${count}`;
|
|
135
|
-
}
|
|
136
|
-
// Add monthly total
|
|
137
|
-
csv += `,${`${data.monthly[month].total}\n`}`;
|
|
138
|
-
}
|
|
139
|
-
// Add overall totals row
|
|
140
|
-
csv += 'Overall';
|
|
141
|
-
for (const author of authors) {
|
|
142
|
-
const count = data.overall[author] || 0;
|
|
143
|
-
csv += `,${count}`;
|
|
144
|
-
}
|
|
145
|
-
csv += `,${data.overall.total}\n`;
|
|
146
|
-
return csv;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Formats the report data as text
|
|
150
|
-
* @param data - The structured report data
|
|
151
|
-
*/
|
|
152
|
-
export function formatAsText(data) {
|
|
153
|
-
let report = 'Monthly Commit Report\n';
|
|
154
|
-
// Monthly sections
|
|
155
|
-
const sortedMonths = Object.keys(data.monthly).sort();
|
|
156
|
-
for (const month of sortedMonths) {
|
|
157
|
-
report += `\n## ${month}\n`;
|
|
158
|
-
const authors = Object.entries(data.monthly[month])
|
|
159
|
-
.filter(([author]) => author !== 'total')
|
|
160
|
-
.sort((a, b) => b[1] - a[1]);
|
|
161
|
-
for (const [author, count] of authors) {
|
|
162
|
-
report += `${count.toString().padStart(6)} ${author}\n`;
|
|
163
|
-
}
|
|
164
|
-
report += `${data.monthly[month].total.toString().padStart(6)} TOTAL\n`;
|
|
165
|
-
}
|
|
166
|
-
// Overall statistics
|
|
167
|
-
report += '\n## Overall Statistics\n';
|
|
168
|
-
const sortedEntries = Object.entries(data.overall)
|
|
169
|
-
.filter(([author]) => author !== 'total')
|
|
170
|
-
.sort((a, b) => b[1] - a[1]);
|
|
171
|
-
for (const [author, count] of sortedEntries) {
|
|
172
|
-
report += `${count.toString().padStart(6)} ${author}\n`;
|
|
173
|
-
}
|
|
174
|
-
report += `${data.overall.total.toString().padStart(6)} GRAND TOTAL\n`;
|
|
175
|
-
return report;
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Format output based on user preference
|
|
179
|
-
* @param output
|
|
180
|
-
* @param reportData
|
|
181
|
-
* @returns
|
|
182
|
-
*/
|
|
183
|
-
export function formatOutputBasedOnFlag(output, reportData) {
|
|
184
|
-
let formattedOutput;
|
|
185
|
-
switch (output) {
|
|
186
|
-
case 'json':
|
|
187
|
-
formattedOutput = JSON.stringify(reportData, null, 2);
|
|
188
|
-
break;
|
|
189
|
-
case 'csv':
|
|
190
|
-
formattedOutput = formatAsCsv(reportData);
|
|
191
|
-
break;
|
|
192
|
-
default:
|
|
193
|
-
formattedOutput = formatAsText(reportData);
|
|
194
|
-
}
|
|
195
|
-
return formattedOutput;
|
|
72
|
+
}, new Map()))
|
|
73
|
+
.map(([key, value]) => ({
|
|
74
|
+
month: key,
|
|
75
|
+
...value,
|
|
76
|
+
}))
|
|
77
|
+
.sort((a, b) => new Date(a.end).valueOf() - new Date(b.end).valueOf());
|
|
196
78
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function decodeJwtPayload(token: string | undefined): Record<string, unknown> | undefined;
|