@herodevs/cli 2.0.0-beta.14 → 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 +148 -16
- 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 +34 -0
- 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 +25 -17
- 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/scan/eol.d.ts +2 -0
- package/dist/commands/scan/eol.js +34 -4
- package/dist/commands/tracker/run.d.ts +15 -0
- package/dist/commands/tracker/run.js +183 -0
- package/dist/config/constants.d.ts +10 -0
- package/dist/config/constants.js +10 -0
- package/dist/config/tracker.config.js +1 -3
- 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/jwt.svc.d.ts +1 -0
- package/dist/service/jwt.svc.js +19 -0
- package/dist/service/tracker.svc.d.ts +56 -1
- package/dist/service/tracker.svc.js +78 -3
- 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 +31 -17
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function decodeJwtPayload(token: string | undefined): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function decodeJwtPayload(token) {
|
|
2
|
+
if (!token) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
try {
|
|
6
|
+
const parts = token.split('.');
|
|
7
|
+
if (parts.length < 2 || !parts[1]) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
11
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
return payload;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,3 +1,58 @@
|
|
|
1
|
-
import { type TrackerConfig } from '../config/tracker.config.
|
|
1
|
+
import { type TrackerCategoryDefinition, type TrackerConfig } from '../config/tracker.config.ts';
|
|
2
|
+
export type GitLastCommit = {
|
|
3
|
+
hash: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
author: string;
|
|
6
|
+
};
|
|
7
|
+
export type FilesStats = {
|
|
8
|
+
total: number;
|
|
9
|
+
source: number;
|
|
10
|
+
comment: number;
|
|
11
|
+
single: number;
|
|
12
|
+
block: number;
|
|
13
|
+
mixed: number;
|
|
14
|
+
empty: number;
|
|
15
|
+
todo: number;
|
|
16
|
+
blockEmpty: number;
|
|
17
|
+
[k: string]: number | string | boolean;
|
|
18
|
+
};
|
|
19
|
+
export type CategoryStatsResult = {
|
|
20
|
+
name: string;
|
|
21
|
+
totals: FilesStats;
|
|
22
|
+
errors: string[];
|
|
23
|
+
fileTypes: string[];
|
|
24
|
+
};
|
|
25
|
+
export type CategorySavedResult = {
|
|
26
|
+
timestamp: string;
|
|
27
|
+
hash: string;
|
|
28
|
+
categories: CategoryStatsResult[];
|
|
29
|
+
};
|
|
30
|
+
export declare const INITIAL_FILES_STATS: FilesStats;
|
|
2
31
|
export declare const getRootDir: (path: string) => string;
|
|
32
|
+
export declare const getConfiguration: (path: string, folderName: string, fileName: string) => TrackerConfig;
|
|
3
33
|
export declare const createTrackerConfig: (rootPath: string, config: TrackerConfig, overwrite?: boolean) => Promise<void>;
|
|
34
|
+
export declare const getFilesFromCategory: (category: TrackerCategoryDefinition, options: {
|
|
35
|
+
rootDir: string;
|
|
36
|
+
ignorePatterns?: string[];
|
|
37
|
+
}) => string[];
|
|
38
|
+
export declare const getFileStats: (path: string, options: {
|
|
39
|
+
rootDir: string;
|
|
40
|
+
}) => {
|
|
41
|
+
source: number;
|
|
42
|
+
total: number;
|
|
43
|
+
comment: number;
|
|
44
|
+
single: number;
|
|
45
|
+
block: number;
|
|
46
|
+
mixed: number;
|
|
47
|
+
empty: number;
|
|
48
|
+
todo: number;
|
|
49
|
+
blockEmpty: number;
|
|
50
|
+
path: string;
|
|
51
|
+
fileType: string;
|
|
52
|
+
error?: undefined;
|
|
53
|
+
} | {
|
|
54
|
+
path: string;
|
|
55
|
+
fileType: string;
|
|
56
|
+
error: boolean;
|
|
57
|
+
};
|
|
58
|
+
export declare const saveResults: (categoriesResult: CategoryStatsResult[], rootDir: string, outputDir: string, git: GitLastCommit) => string;
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { writeFile } from 'node:fs/promises';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
3
|
+
import { extname, join, resolve } from 'node:path';
|
|
4
|
+
import { globSync } from 'glob'; // replace with node:fs globSync as soon as v22 is minimum supported version
|
|
5
|
+
import sloc from 'sloc';
|
|
6
|
+
import { DEFAULT_TRACKER_RUN_DATA_FILE } from '../config/constants.js';
|
|
7
|
+
import { TRACKER_ROOT_FILE } from "../config/tracker.config.js";
|
|
8
|
+
export const INITIAL_FILES_STATS = {
|
|
9
|
+
total: 0,
|
|
10
|
+
block: 0,
|
|
11
|
+
blockEmpty: 0,
|
|
12
|
+
comment: 0,
|
|
13
|
+
empty: 0,
|
|
14
|
+
mixed: 0,
|
|
15
|
+
single: 0,
|
|
16
|
+
source: 0,
|
|
17
|
+
todo: 0,
|
|
18
|
+
};
|
|
5
19
|
export const getRootDir = (path) => {
|
|
6
20
|
if (existsSync(join(path, TRACKER_ROOT_FILE))) {
|
|
7
21
|
return path;
|
|
@@ -11,6 +25,21 @@ export const getRootDir = (path) => {
|
|
|
11
25
|
}
|
|
12
26
|
return getRootDir(resolve(join(path, '..')));
|
|
13
27
|
};
|
|
28
|
+
export const getConfiguration = (path, folderName, fileName) => {
|
|
29
|
+
const filePath = join(path, folderName, fileName);
|
|
30
|
+
if (!existsSync(filePath)) {
|
|
31
|
+
throw new Error(`Couldn't find configuration ${fileName} file in ${path}. If you haven't, run tracker init command to create the configuration file. If you have a custom folder and configuration file, use the flags -d (directory) and -f (filename) to specify it`);
|
|
32
|
+
}
|
|
33
|
+
const stringConfiguration = readFileSync(filePath, {
|
|
34
|
+
encoding: 'utf-8',
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(stringConfiguration);
|
|
38
|
+
}
|
|
39
|
+
catch (_err) {
|
|
40
|
+
throw new Error(`A configuration file was found, but it's contents are not valid. Review your configuration file and fix any errors, or run tracker init -o to overwrite the file`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
14
43
|
export const createTrackerConfig = async (rootPath, config, overwrite = false) => {
|
|
15
44
|
const { outputDir } = config;
|
|
16
45
|
const configDir = join(rootPath, outputDir);
|
|
@@ -24,3 +53,49 @@ export const createTrackerConfig = async (rootPath, config, overwrite = false) =
|
|
|
24
53
|
}
|
|
25
54
|
await writeFile(join(configDir, config.configFile), JSON.stringify(config, null, 2));
|
|
26
55
|
};
|
|
56
|
+
export const getFilesFromCategory = (category, options) => {
|
|
57
|
+
const { fileTypes, includes } = category;
|
|
58
|
+
const { rootDir, ignorePatterns } = options;
|
|
59
|
+
// if no includes folder or no specific file type is set, we ignore the category
|
|
60
|
+
if (fileTypes.length === 0 || includes.length === 0) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const patterns = includes.flatMap((include) => fileTypes.map((type) => `*${include.replace('./', '')}/**/*.${type}`));
|
|
64
|
+
return globSync(patterns, {
|
|
65
|
+
cwd: rootDir,
|
|
66
|
+
ignore: ignorePatterns?.map((ignore) => `${ignore}`) ?? [],
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
export const getFileStats = (path, options) => {
|
|
70
|
+
const fileType = extname(path).replace(/\./g, '');
|
|
71
|
+
try {
|
|
72
|
+
const stats = sloc(readFileSync(join(options.rootDir, path), 'utf8'), fileType);
|
|
73
|
+
return {
|
|
74
|
+
path,
|
|
75
|
+
fileType,
|
|
76
|
+
...stats,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (_err) {
|
|
80
|
+
return {
|
|
81
|
+
path,
|
|
82
|
+
fileType,
|
|
83
|
+
error: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
export const saveResults = (categoriesResult, rootDir, outputDir, git) => {
|
|
88
|
+
const dataResults = [];
|
|
89
|
+
const dataFile = join(rootDir, outputDir, DEFAULT_TRACKER_RUN_DATA_FILE);
|
|
90
|
+
try {
|
|
91
|
+
const savedOutput = readFileSync(dataFile).toString('utf8');
|
|
92
|
+
dataResults.push(...JSON.parse(savedOutput));
|
|
93
|
+
}
|
|
94
|
+
catch (_err) { }
|
|
95
|
+
dataResults.push({
|
|
96
|
+
...git,
|
|
97
|
+
categories: categoriesResult,
|
|
98
|
+
});
|
|
99
|
+
writeFileSync(dataFile, JSON.stringify(dataResults, null, 2));
|
|
100
|
+
return `${dataFile}`;
|
|
101
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function openInBrowser(url: string): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { platform } from 'node:os';
|
|
3
|
+
export function openInBrowser(url) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const escapedUrl = `"${url.replace(/"/g, '\\"')}"`;
|
|
6
|
+
const command = (() => {
|
|
7
|
+
const plat = platform();
|
|
8
|
+
if (plat === 'darwin')
|
|
9
|
+
return `open ${escapedUrl}`; // macOS
|
|
10
|
+
if (plat === 'win32')
|
|
11
|
+
return `start "" ${escapedUrl}`; // Windows
|
|
12
|
+
return `xdg-open ${escapedUrl}`; // Linux
|
|
13
|
+
})();
|
|
14
|
+
exec(command, (err) => {
|
|
15
|
+
if (err)
|
|
16
|
+
reject(new Error(`Failed to open browser: ${err.message}`));
|
|
17
|
+
else
|
|
18
|
+
resolve();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RetryOptions = {
|
|
2
|
+
attempts: number;
|
|
3
|
+
baseDelayMs: number;
|
|
4
|
+
onRetry?: (info: {
|
|
5
|
+
attempt: number;
|
|
6
|
+
delayMs: number;
|
|
7
|
+
error: unknown;
|
|
8
|
+
}) => void;
|
|
9
|
+
finalErrorMessage?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function withRetries<T>(operation: string, fn: () => Promise<T>, options: RetryOptions): Promise<T>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { debugLogger } from "../service/log.svc.js";
|
|
2
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
export async function withRetries(operation, fn, options) {
|
|
4
|
+
const { attempts, baseDelayMs, onRetry, finalErrorMessage } = options;
|
|
5
|
+
let lastError;
|
|
6
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
7
|
+
try {
|
|
8
|
+
return await fn();
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
lastError = error;
|
|
12
|
+
if (attempt === attempts) {
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
const delayMs = baseDelayMs * attempt;
|
|
16
|
+
if (onRetry) {
|
|
17
|
+
onRetry({ attempt, delayMs, error });
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
debugLogger('Retry (%s) attempt %d/%d after %dms: %o', operation, attempt, attempts, delayMs, error);
|
|
21
|
+
}
|
|
22
|
+
await sleep(delayMs);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const message = finalErrorMessage ??
|
|
26
|
+
(lastError instanceof Error ? lastError.message : null) ??
|
|
27
|
+
'Please contact support@herodevs.com.';
|
|
28
|
+
throw new Error(message);
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herodevs/cli",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.15",
|
|
4
4
|
"author": "HeroDevs, Inc",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "./bin/run.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"url": "https://github.com/herodevs/cli"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/herodevs/cli",
|
|
14
|
-
"bugs": "https://github.com
|
|
14
|
+
"bugs": "https://github.com/herodevs/cli/issues",
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "shx rm -rf dist && tsc -b",
|
|
17
17
|
"ci": "biome ci",
|
|
@@ -20,15 +20,18 @@
|
|
|
20
20
|
"clean:files": "shx rm -f herodevs.**.csv herodevs.**.json herodevs.**.txt",
|
|
21
21
|
"dev": "npm run build && ./bin/dev.js",
|
|
22
22
|
"dev:debug": "npm run build && DEBUG=oclif:* ./bin/dev.js",
|
|
23
|
+
"dev:auth:local": "OAUTH_CONNECT_URL='http://localhost:6040/realms/herodevs_local/protocol/openid-connect' npm run dev auth login",
|
|
24
|
+
"dev:auth:logout": "npm run dev auth logout",
|
|
23
25
|
"format": "biome format --write",
|
|
24
26
|
"lint": "biome lint --write",
|
|
25
27
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
26
28
|
"prepare": "shx test -d dist || npm run build",
|
|
27
29
|
"prepack": "oclif manifest",
|
|
28
30
|
"pretest": "npm run lint && npm run typecheck",
|
|
29
|
-
"readme": "npm run ci:fix && npm run build && oclif readme",
|
|
30
|
-
"test": "
|
|
31
|
-
"test:
|
|
31
|
+
"readme": "npm run ci:fix && npm run build && oclif readme && sed -i '' 's|/plugin-help/blob/v|/plugin-help/blob/|; s|/plugin-update/blob/v|/plugin-update/blob/|' README.md",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest watch",
|
|
34
|
+
"test:e2e": "globstar -- node --import tsx --import ./e2e/setup/register-mock-auth.mjs --test \"e2e/**/*.test.ts\"",
|
|
32
35
|
"typecheck": "tsc --noEmit",
|
|
33
36
|
"version": "oclif manifest",
|
|
34
37
|
"postversion": "node scripts/update-install-script-version.js && git add README.md"
|
|
@@ -39,29 +42,40 @@
|
|
|
39
42
|
"herodevs cli"
|
|
40
43
|
],
|
|
41
44
|
"dependencies": {
|
|
42
|
-
"@amplitude/analytics-node": "^1.5.
|
|
45
|
+
"@amplitude/analytics-node": "^1.5.26",
|
|
43
46
|
"@apollo/client": "^4.0.9",
|
|
44
|
-
"@cyclonedx/cdxgen": "^
|
|
45
|
-
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.
|
|
46
|
-
"@inquirer/prompts": "^8.0.
|
|
47
|
+
"@cyclonedx/cdxgen": "^12.1.1",
|
|
48
|
+
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.17",
|
|
49
|
+
"@inquirer/prompts": "^8.0.2",
|
|
50
|
+
"@napi-rs/keyring": "^1.2.0",
|
|
47
51
|
"@oclif/core": "^4.8.0",
|
|
48
52
|
"@oclif/plugin-help": "^6.2.32",
|
|
49
|
-
"@oclif/plugin-update": "^4.7.
|
|
53
|
+
"@oclif/plugin-update": "^4.7.16",
|
|
50
54
|
"@oclif/table": "^0.5.1",
|
|
55
|
+
"cli-progress": "^3.12.0",
|
|
56
|
+
"conf": "^15.1.0",
|
|
51
57
|
"date-fns": "^4.1.0",
|
|
58
|
+
"glob": "^13.0.0",
|
|
59
|
+
"graphql": "^16.11.0",
|
|
52
60
|
"node-machine-id": "^1.1.12",
|
|
53
61
|
"ora": "^9.0.0",
|
|
54
62
|
"packageurl-js": "^2.0.1",
|
|
63
|
+
"sloc": "^0.3.2",
|
|
55
64
|
"terminal-link": "^5.0.0",
|
|
56
65
|
"update-notifier": "^7.3.1"
|
|
57
66
|
},
|
|
58
67
|
"devDependencies": {
|
|
59
|
-
"@biomejs/biome": "^2.3.
|
|
60
|
-
"@oclif/test": "^4.1.
|
|
68
|
+
"@biomejs/biome": "^2.3.8",
|
|
69
|
+
"@oclif/test": "^4.1.15",
|
|
70
|
+
"@types/cli-progress": "^3.11.6",
|
|
71
|
+
"@types/debug": "^4.1.12",
|
|
61
72
|
"@types/inquirer": "^9.0.9",
|
|
62
73
|
"@types/mock-fs": "^4.13.4",
|
|
63
|
-
"@types/node": "^24.10.
|
|
64
|
-
"@types/
|
|
74
|
+
"@types/node": "^24.10.1",
|
|
75
|
+
"@types/ora": "^3.1.0",
|
|
76
|
+
"@types/sinon": "^21.0.0",
|
|
77
|
+
"@types/sloc": "^0.2.3",
|
|
78
|
+
"@types/terminal-link": "^1.1.0",
|
|
65
79
|
"@types/update-notifier": "^6.0.8",
|
|
66
80
|
"globstar": "^1.0.0",
|
|
67
81
|
"mock-fs": "^5.5.0",
|
|
@@ -69,8 +83,9 @@
|
|
|
69
83
|
"shx": "^0.4.0",
|
|
70
84
|
"sinon": "^21.0.0",
|
|
71
85
|
"ts-node": "^10.9.2",
|
|
72
|
-
"tsx": "^4.
|
|
73
|
-
"typescript": "^5.9.3"
|
|
86
|
+
"tsx": "^4.21.0",
|
|
87
|
+
"typescript": "^5.9.3",
|
|
88
|
+
"vitest": "^4.0.16"
|
|
74
89
|
},
|
|
75
90
|
"engines": {
|
|
76
91
|
"node": ">=20.0.0"
|
|
@@ -88,7 +103,6 @@
|
|
|
88
103
|
"commands": "./dist/commands",
|
|
89
104
|
"plugins": [
|
|
90
105
|
"@oclif/plugin-help",
|
|
91
|
-
"@oclif/plugin-plugins",
|
|
92
106
|
"@oclif/plugin-update"
|
|
93
107
|
],
|
|
94
108
|
"hooks": {
|