@herodevs/cli 2.0.0-beta.1 → 2.0.0-beta.11

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 (69) hide show
  1. package/README.md +201 -110
  2. package/bin/dev.js +1 -4
  3. package/bin/main.js +3 -3
  4. package/bin/run.js +1 -1
  5. package/dist/api/gql-operations.d.ts +2 -0
  6. package/dist/api/gql-operations.js +36 -0
  7. package/dist/api/nes.client.d.ts +12 -0
  8. package/dist/api/nes.client.js +84 -0
  9. package/dist/commands/scan/eol.d.ts +18 -19
  10. package/dist/commands/scan/eol.js +214 -142
  11. package/dist/config/constants.d.ts +9 -3
  12. package/dist/config/constants.js +19 -3
  13. package/dist/hooks/finally/finally.d.ts +3 -0
  14. package/dist/hooks/finally/finally.js +18 -0
  15. package/dist/hooks/{npm-update-notifier.js → init/00_npm-update-notifier.js} +3 -3
  16. package/dist/hooks/init/01_initialize_amplitude.d.ts +3 -0
  17. package/dist/hooks/init/01_initialize_amplitude.js +15 -0
  18. package/dist/service/analytics.svc.d.ts +27 -0
  19. package/dist/service/analytics.svc.js +112 -0
  20. package/dist/service/{eol/cdx.svc.d.ts → cdx.svc.d.ts} +8 -16
  21. package/dist/service/{eol/cdx.svc.js → cdx.svc.js} +17 -7
  22. package/dist/service/display.svc.d.ts +30 -0
  23. package/dist/service/display.svc.js +87 -0
  24. package/dist/service/file.svc.d.ts +30 -0
  25. package/dist/service/file.svc.js +115 -0
  26. package/dist/service/log.svc.d.ts +1 -0
  27. package/dist/service/log.svc.js +9 -0
  28. package/dist/service/{eol/sbom.worker.js → sbom.worker.js} +2 -1
  29. package/dist/utils/strip-typename.d.ts +1 -0
  30. package/dist/utils/strip-typename.js +15 -0
  31. package/package.json +33 -22
  32. package/dist/api/client.d.ts +0 -12
  33. package/dist/api/client.js +0 -43
  34. package/dist/api/nes/nes.client.d.ts +0 -23
  35. package/dist/api/nes/nes.client.js +0 -107
  36. package/dist/api/queries/nes/sbom.d.ts +0 -3
  37. package/dist/api/queries/nes/sbom.js +0 -35
  38. package/dist/api/queries/nes/telemetry.d.ts +0 -2
  39. package/dist/api/queries/nes/telemetry.js +0 -24
  40. package/dist/api/types/hd-cli.types.d.ts +0 -30
  41. package/dist/api/types/hd-cli.types.js +0 -10
  42. package/dist/api/types/nes.types.d.ts +0 -53
  43. package/dist/api/types/nes.types.js +0 -1
  44. package/dist/commands/report/committers.d.ts +0 -23
  45. package/dist/commands/report/committers.js +0 -146
  46. package/dist/commands/report/purls.d.ts +0 -15
  47. package/dist/commands/report/purls.js +0 -84
  48. package/dist/commands/scan/sbom.d.ts +0 -21
  49. package/dist/commands/scan/sbom.js +0 -159
  50. package/dist/service/committers.svc.d.ts +0 -70
  51. package/dist/service/committers.svc.js +0 -196
  52. package/dist/service/eol/eol.svc.d.ts +0 -14
  53. package/dist/service/eol/eol.svc.js +0 -49
  54. package/dist/service/error.svc.d.ts +0 -8
  55. package/dist/service/error.svc.js +0 -28
  56. package/dist/service/nes/nes.svc.d.ts +0 -5
  57. package/dist/service/nes/nes.svc.js +0 -27
  58. package/dist/service/purls.svc.d.ts +0 -23
  59. package/dist/service/purls.svc.js +0 -99
  60. package/dist/ui/date.ui.d.ts +0 -1
  61. package/dist/ui/date.ui.js +0 -15
  62. package/dist/ui/eol.ui.d.ts +0 -15
  63. package/dist/ui/eol.ui.js +0 -134
  64. package/dist/ui/shared.ui.d.ts +0 -6
  65. package/dist/ui/shared.ui.js +0 -16
  66. /package/dist/hooks/{npm-update-notifier.d.ts → init/00_npm-update-notifier.d.ts} +0 -0
  67. /package/dist/hooks/{prerun.d.ts → prerun/prerun.d.ts} +0 -0
  68. /package/dist/hooks/{prerun.js → prerun/prerun.js} +0 -0
  69. /package/dist/service/{eol/sbom.worker.d.ts → sbom.worker.d.ts} +0 -0
@@ -0,0 +1,112 @@
1
+ import os from 'node:os';
2
+ import { track as _track, Identify, identify, init, setOptOut, Types } from '@amplitude/analytics-node';
3
+ import NodeMachineId from 'node-machine-id';
4
+ import { config } from "../config/constants.js";
5
+ const device_id = NodeMachineId.machineIdSync(true);
6
+ const started_at = new Date();
7
+ const session_id = started_at.getTime();
8
+ const defaultAnalyticsContext = {
9
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
10
+ os_platform: os.platform(),
11
+ os_release: os.release(),
12
+ cli_version: process.env.npm_package_version ?? 'unknown',
13
+ ci_provider: getCIProvider(),
14
+ app_used: getTerminal(),
15
+ started_at,
16
+ };
17
+ let analyticsContext = defaultAnalyticsContext;
18
+ export function initializeAnalytics() {
19
+ init('0', {
20
+ flushQueueSize: 2,
21
+ flushIntervalMillis: 250,
22
+ logLevel: Types.LogLevel.None,
23
+ serverUrl: config.analyticsUrl,
24
+ });
25
+ setOptOut(process.env.TRACKING_OPT_OUT === 'true');
26
+ identify(new Identify(), {
27
+ device_id,
28
+ platform: analyticsContext.os_platform,
29
+ os_name: getOSName(analyticsContext.os_platform ?? ''),
30
+ os_version: analyticsContext.os_release,
31
+ session_id,
32
+ app_version: analyticsContext.cli_version,
33
+ });
34
+ }
35
+ export function track(event, getProperties) {
36
+ const localContext = getProperties?.(analyticsContext);
37
+ if (localContext) {
38
+ analyticsContext = { ...analyticsContext, ...localContext };
39
+ }
40
+ return _track(event, localContext, { device_id, session_id });
41
+ }
42
+ function getCIProvider(env = process.env) {
43
+ if (env.GITHUB_ACTIONS)
44
+ return 'github';
45
+ if (env.GITLAB_CI)
46
+ return 'gitlab';
47
+ if (env.CIRCLECI)
48
+ return 'circleci';
49
+ if (env.TF_BUILD)
50
+ return 'azure';
51
+ if (env.BITBUCKET_COMMIT || env.BITBUCKET_BUILD_NUMBER)
52
+ return 'bitbucket';
53
+ if (env.JENKINS_URL)
54
+ return 'jenkins';
55
+ if (env.BUILDKITE)
56
+ return 'buildkite';
57
+ if (env.TRAVIS)
58
+ return 'travis';
59
+ if (env.TEAMCITY_VERSION)
60
+ return 'teamcity';
61
+ if (env.CODEBUILD_BUILD_ID)
62
+ return 'codebuild';
63
+ if (env.CI)
64
+ return 'unknown_ci';
65
+ return undefined;
66
+ }
67
+ function getTerminal(env = process.env) {
68
+ if (env.TERM_PROGRAM === 'vscode' || env.VSCODE_PID)
69
+ return 'vscode';
70
+ if (env.TERM_PROGRAM === 'iTerm.app' || env.ITERM_SESSION_ID)
71
+ return 'iterm';
72
+ if (env.TERM_PROGRAM === 'Apple_Terminal')
73
+ return 'apple_terminal';
74
+ if (env.TERM_PROGRAM === 'WarpTerminal' || env.WARP_IS_LOCAL_SHELL_SESSION)
75
+ return 'warp';
76
+ if (env.TERM_PROGRAM === 'ghostty' || env.GHOSTTY_RESOURCES_DIR || env.GHOSTTY_CONFIG_DIR)
77
+ return 'ghostty';
78
+ if (env.TERM_PROGRAM === 'WezTerm' || env.WEZTERM_EXECUTABLE || env.WEZTERM_PANE)
79
+ return 'wezterm';
80
+ if (env.TERM === 'alacritty' || env.ALACRITTY_LOG || env.ALACRITTY_SOCKET)
81
+ return 'alacritty';
82
+ if (env.TERM === 'xterm-kitty' || env.KITTY_WINDOW_ID)
83
+ return 'kitty';
84
+ if (env.WT_SESSION || env.WT_PROFILE_ID)
85
+ return 'windows_terminal';
86
+ if (env.ConEmuPID || env.ConEmuDir || env.CONEMU_BUILD)
87
+ return 'conemu';
88
+ if (env.TERM_PROGRAM === 'mintty' || env.MINTTY_SHORTCUT)
89
+ return 'mintty';
90
+ if (env.TILIX_ID)
91
+ return 'tilix';
92
+ if (env.GNOME_TERMINAL_SCREEN || env.GNOME_TERMINAL_SERVICE || env.VTE_VERSION)
93
+ return 'gnome';
94
+ if (env.KONSOLE_VERSION)
95
+ return 'konsole';
96
+ if (env.TERM_PROGRAM === 'Hyper')
97
+ return 'hyper';
98
+ return 'unknown_terminal';
99
+ }
100
+ function getOSName(platform) {
101
+ if (platform === 'darwin')
102
+ return 'macOS';
103
+ if (platform === 'win32')
104
+ return 'Windows';
105
+ if (platform === 'linux')
106
+ return 'Linux';
107
+ if (platform === 'android')
108
+ return 'Android';
109
+ if (platform === 'ios')
110
+ return 'iOS';
111
+ return platform;
112
+ }
@@ -1,18 +1,4 @@
1
- import type { CdxGenOptions } from './eol.svc.ts';
2
- export interface SbomDependency {
3
- ref: string;
4
- dependsOn: string[];
5
- }
6
- export interface SbomEntry {
7
- group: string;
8
- name: string;
9
- purl: string;
10
- version: string;
11
- }
12
- export interface Sbom {
13
- components: SbomEntry[];
14
- dependencies: SbomDependency[];
15
- }
1
+ import type { CdxBom } from '@herodevs/eol-shared';
16
2
  export declare const SBOM_DEFAULT__OPTIONS: {
17
3
  $0: string;
18
4
  _: never[];
@@ -44,6 +30,7 @@ export declare const SBOM_DEFAULT__OPTIONS: {
44
30
  o: string;
45
31
  output: string;
46
32
  outputFormat: string;
33
+ author: string[];
47
34
  profile: string;
48
35
  project: undefined;
49
36
  'project-version': string;
@@ -61,6 +48,11 @@ export declare const SBOM_DEFAULT__OPTIONS: {
61
48
  skipDtTlsCheck: boolean;
62
49
  'spec-version': number;
63
50
  specVersion: number;
51
+ tools: {
52
+ name: string;
53
+ publisher: string;
54
+ version: string;
55
+ }[];
64
56
  'usages-slices-file': string;
65
57
  usagesSlicesFile: string;
66
58
  validate: boolean;
@@ -69,4 +61,4 @@ export declare const SBOM_DEFAULT__OPTIONS: {
69
61
  * Lazy loads cdxgen (for ESM purposes), scans
70
62
  * `directory`, and returns the `bomJson` property.
71
63
  */
72
- export declare function createBomFromDir(directory: string, opts?: CdxGenOptions): Promise<any>;
64
+ export declare function createSbom(directory: string): Promise<CdxBom>;
@@ -1,5 +1,6 @@
1
1
  import { createBom } from '@cyclonedx/cdxgen';
2
- import { debugLogger } from "../../service/log.svc.js";
2
+ import { debugLogger } from "./log.svc.js";
3
+ const author = process.env.npm_package_author ?? 'HeroDevs, Inc.';
3
4
  export const SBOM_DEFAULT__OPTIONS = {
4
5
  $0: 'cdxgen',
5
6
  _: [],
@@ -23,8 +24,8 @@ export const SBOM_DEFAULT__OPTIONS = {
23
24
  includeFormulation: false,
24
25
  'no-install-deps': true,
25
26
  noInstallDeps: true,
26
- 'min-confidence': 0,
27
- minConfidence: 0,
27
+ 'min-confidence': 1,
28
+ minConfidence: 1,
28
29
  multiProject: true,
29
30
  'no-banner': false,
30
31
  noBabel: false,
@@ -32,7 +33,7 @@ export const SBOM_DEFAULT__OPTIONS = {
32
33
  o: 'bom.json',
33
34
  output: 'bom.json',
34
35
  outputFormat: 'json', // or "xml"
35
- // author: ['OWASP Foundation'],
36
+ author: [author],
36
37
  profile: 'generic',
37
38
  project: undefined,
38
39
  'project-version': '',
@@ -50,6 +51,13 @@ export const SBOM_DEFAULT__OPTIONS = {
50
51
  skipDtTlsCheck: true,
51
52
  'spec-version': 1.6,
52
53
  specVersion: 1.6,
54
+ tools: [
55
+ {
56
+ name: '@herodevs/cli',
57
+ publisher: author,
58
+ version: process.env.npm_package_version ?? 'unknown',
59
+ },
60
+ ],
53
61
  'usages-slices-file': 'usages.slices.json',
54
62
  usagesSlicesFile: 'usages.slices.json',
55
63
  validate: true,
@@ -58,8 +66,10 @@ export const SBOM_DEFAULT__OPTIONS = {
58
66
  * Lazy loads cdxgen (for ESM purposes), scans
59
67
  * `directory`, and returns the `bomJson` property.
60
68
  */
61
- export async function createBomFromDir(directory, opts = {}) {
62
- const sbom = await createBom(directory, { ...SBOM_DEFAULT__OPTIONS, ...opts });
69
+ export async function createSbom(directory) {
70
+ const sbom = await createBom(directory, SBOM_DEFAULT__OPTIONS);
71
+ if (!sbom)
72
+ throw new Error('SBOM not generated');
63
73
  debugLogger('Successfully generated SBOM');
64
- return sbom?.bomJson;
74
+ return sbom.bomJson;
65
75
  }
@@ -0,0 +1,30 @@
1
+ import type { ComponentStatus, EolReport } from '@herodevs/eol-shared';
2
+ /**
3
+ * Formats status row text with appropriate color and icon
4
+ */
5
+ export declare const getStatusRowText: Record<ComponentStatus, (text: string) => string>;
6
+ export type ComponentCounts = Record<ComponentStatus, number> & {
7
+ NES_AVAILABLE: number;
8
+ TOTAL: number;
9
+ ECOSYSTEMS: string[];
10
+ };
11
+ /**
12
+ * Counts components by their status, including NES remediation availability
13
+ */
14
+ export declare function countComponentsByStatus(report: EolReport): ComponentCounts;
15
+ /**
16
+ * Formats scan results for console display
17
+ */
18
+ export declare function formatScanResults(report: EolReport): string[];
19
+ /**
20
+ * Formats web report URL for console display
21
+ */
22
+ export declare function formatWebReportUrl(id: string, reportCardUrl: string): string[];
23
+ /**
24
+ * Formats data privacy information link for console display
25
+ */
26
+ export declare function formatDataPrivacyLink(): string[];
27
+ /**
28
+ * Formats the report save hint for console display when the web report URL is hidden
29
+ */
30
+ export declare function formatReportSaveHint(): string[];
@@ -0,0 +1,87 @@
1
+ import { deriveComponentStatus } from '@herodevs/eol-shared';
2
+ import { ux } from '@oclif/core';
3
+ import terminalLink from 'terminal-link';
4
+ const STATUS_COLORS = {
5
+ EOL: 'red',
6
+ UNKNOWN: 'default',
7
+ OK: 'green',
8
+ EOL_UPCOMING: 'yellow',
9
+ };
10
+ const SEPARATOR_WIDTH = 40;
11
+ /**
12
+ * Formats status row text with appropriate color and icon
13
+ */
14
+ export const getStatusRowText = {
15
+ EOL: (text) => ux.colorize(STATUS_COLORS.EOL, `✗ ${text}`),
16
+ UNKNOWN: (text) => ux.colorize(STATUS_COLORS.UNKNOWN, `• ${text}`),
17
+ OK: (text) => ux.colorize(STATUS_COLORS.OK, `✔ ${text}`),
18
+ EOL_UPCOMING: (text) => ux.colorize(STATUS_COLORS.EOL_UPCOMING, `! ${text}`),
19
+ };
20
+ /**
21
+ * Counts components by their status, including NES remediation availability
22
+ */
23
+ export function countComponentsByStatus(report) {
24
+ const grouped = {
25
+ UNKNOWN: 0,
26
+ OK: 0,
27
+ EOL_UPCOMING: 0,
28
+ EOL: 0,
29
+ NES_AVAILABLE: 0,
30
+ ECOSYSTEMS: [],
31
+ TOTAL: report.components.length,
32
+ };
33
+ const ecosystems = new Set();
34
+ for (const component of report.components) {
35
+ const status = deriveComponentStatus(component.metadata);
36
+ grouped[status]++;
37
+ if (component.nesRemediation?.remediations?.length) {
38
+ grouped.NES_AVAILABLE++;
39
+ }
40
+ const ecosystem = component.purl.match(/^pkg:([^/]+)\//)?.[1];
41
+ if (ecosystem) {
42
+ ecosystems.add(ecosystem);
43
+ }
44
+ }
45
+ grouped.ECOSYSTEMS = Array.from(ecosystems);
46
+ return grouped;
47
+ }
48
+ /**
49
+ * Formats scan results for console display
50
+ */
51
+ export function formatScanResults(report) {
52
+ const { UNKNOWN, OK, EOL_UPCOMING, EOL, NES_AVAILABLE } = countComponentsByStatus(report);
53
+ if (!UNKNOWN && !OK && !EOL_UPCOMING && !EOL) {
54
+ return [ux.colorize('yellow', 'No components found in scan.')];
55
+ }
56
+ return [
57
+ ux.colorize('bold', 'Scan results:'),
58
+ ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)),
59
+ ux.colorize('bold', `${report.components.length.toLocaleString()} total packages scanned`),
60
+ getStatusRowText.EOL(`${EOL.toLocaleString().padEnd(5)} End-of-Life (EOL)`),
61
+ getStatusRowText.EOL_UPCOMING(`${EOL_UPCOMING.toLocaleString().padEnd(5)} EOL Upcoming`),
62
+ getStatusRowText.OK(`${OK.toLocaleString().padEnd(5)} Not End-of-Life (EOL)`),
63
+ getStatusRowText.UNKNOWN(`${UNKNOWN.toLocaleString().padEnd(5)} Unknown EOL Status`),
64
+ getStatusRowText.UNKNOWN(`${NES_AVAILABLE.toLocaleString().padEnd(5)} HeroDevs NES Remediation${NES_AVAILABLE !== 1 ? 's' : ''} Available`),
65
+ ];
66
+ }
67
+ /**
68
+ * Formats web report URL for console display
69
+ */
70
+ export function formatWebReportUrl(id, reportCardUrl) {
71
+ const url = ux.colorize('blue', terminalLink(new URL(reportCardUrl).hostname, `${reportCardUrl}/${id}`, { fallback: (_, url) => url }));
72
+ return [ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)), `🌐 View your full EOL report at: ${url}\n`];
73
+ }
74
+ /**
75
+ * Formats data privacy information link for console display
76
+ */
77
+ export function formatDataPrivacyLink() {
78
+ const privacyUrl = 'https://docs.herodevs.com/eol-ds/data-privacy-and-security';
79
+ const link = ux.colorize('blue', terminalLink('Learn more about data privacy', privacyUrl, { fallback: (text, url) => `${text}: ${url}` }));
80
+ return [`🔒 ${link}\n`];
81
+ }
82
+ /**
83
+ * Formats the report save hint for console display when the web report URL is hidden
84
+ */
85
+ export function formatReportSaveHint() {
86
+ return [ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)), 'To save your detailed JSON report, use the --save flag'];
87
+ }
@@ -0,0 +1,30 @@
1
+ import type { CdxBom, EolReport } from '@herodevs/eol-shared';
2
+ export interface FileError extends Error {
3
+ code?: string;
4
+ }
5
+ /**
6
+ * Reads an SBOM from a file path and converts it to CycloneDX format
7
+ * Supports both SPDX 2.3 and CycloneDX formats
8
+ */
9
+ export declare function readSbomFromFile(filePath: string): CdxBom;
10
+ /**
11
+ * Validates that a directory path exists and is actually a directory
12
+ */
13
+ export declare function validateDirectory(dirPath: string): void;
14
+ type SaveArtifactRequest = {
15
+ kind: 'sbom';
16
+ payload: CdxBom;
17
+ outputPath?: string;
18
+ } | {
19
+ kind: 'sbomTrimmed';
20
+ payload: CdxBom;
21
+ } | {
22
+ kind: 'report';
23
+ payload: EolReport;
24
+ outputPath?: string;
25
+ };
26
+ /**
27
+ * Saves an SBOM, trimmed SBOM, or report to disk using the correct default filename.
28
+ */
29
+ export declare function saveArtifactToFile(dir: string, request: SaveArtifactRequest): string;
30
+ export {};
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs';
2
+ import path, { join, resolve } from 'node:path';
3
+ import { isCdxBom, isSpdxBom, spdxToCdxBom } from '@herodevs/eol-shared';
4
+ import { filenamePrefix } from "../config/constants.js";
5
+ import { getErrorMessage } from "./log.svc.js";
6
+ /**
7
+ * Computes an absolute output path using either a provided path or the base directory and default name.
8
+ */
9
+ function resolveOutputPath(baseDir, defaultFilename, customPath) {
10
+ const defaultOutput = resolve(join(baseDir, defaultFilename));
11
+ if (!customPath) {
12
+ return { fileName: defaultFilename, fullPath: defaultOutput };
13
+ }
14
+ const resolvedCustomPath = resolve(customPath);
15
+ let targetPath = resolvedCustomPath;
16
+ const hasTrailingSeparator = /[\\/]$/.test(customPath);
17
+ const customIsDirectory = fs.existsSync(resolvedCustomPath) && fs.statSync(resolvedCustomPath).isDirectory();
18
+ if (hasTrailingSeparator || customIsDirectory) {
19
+ targetPath = join(resolvedCustomPath, defaultFilename);
20
+ }
21
+ return { fileName: path.basename(targetPath), fullPath: targetPath };
22
+ }
23
+ /**
24
+ * Ensures the output directory for a given path exists, is a directory, and is writable.
25
+ */
26
+ function ensureOutputDirectory(fullPath, fileName) {
27
+ const targetDir = path.dirname(fullPath);
28
+ if (!fs.existsSync(targetDir)) {
29
+ throw new Error(`Unable to save ${fileName}`);
30
+ }
31
+ const stats = fs.statSync(targetDir);
32
+ if (!stats.isDirectory()) {
33
+ throw new Error(`Unable to save ${fileName}`);
34
+ }
35
+ try {
36
+ fs.accessSync(targetDir, fs.constants.W_OK);
37
+ }
38
+ catch {
39
+ throw new Error(`Unable to save ${fileName}`);
40
+ }
41
+ }
42
+ /**
43
+ * Writes JSON to disk after validating directory constraints and formats the payload for readability.
44
+ */
45
+ function writeJsonFile(fullPath, fileName, payload, failureLabel) {
46
+ ensureOutputDirectory(fullPath, fileName);
47
+ try {
48
+ fs.writeFileSync(fullPath, JSON.stringify(payload, null, 2));
49
+ return fullPath;
50
+ }
51
+ catch (error) {
52
+ const fileError = error;
53
+ switch (fileError.code) {
54
+ case 'EACCES':
55
+ throw new Error(`Permission denied. Unable to save ${fileName}`);
56
+ case 'ENOSPC':
57
+ throw new Error(`No space left on device. Unable to save ${fileName}`);
58
+ case 'ENOENT':
59
+ case 'ENOTDIR':
60
+ throw new Error(`Unable to save ${fileName}`);
61
+ }
62
+ throw new Error(`Failed to save ${failureLabel}: ${getErrorMessage(error)}`);
63
+ }
64
+ }
65
+ /**
66
+ * Reads an SBOM from a file path and converts it to CycloneDX format
67
+ * Supports both SPDX 2.3 and CycloneDX formats
68
+ */
69
+ export function readSbomFromFile(filePath) {
70
+ const file = resolve(filePath);
71
+ if (!fs.existsSync(file)) {
72
+ throw new Error(`SBOM file not found: ${file}`);
73
+ }
74
+ try {
75
+ const fileContent = fs.readFileSync(file, 'utf8');
76
+ const jsonContent = JSON.parse(fileContent);
77
+ if (isSpdxBom(jsonContent)) {
78
+ return spdxToCdxBom(jsonContent);
79
+ }
80
+ if (isCdxBom(jsonContent)) {
81
+ return jsonContent;
82
+ }
83
+ throw new Error(`Invalid SBOM file format. Expected SPDX 2.3 or CycloneDX format.`);
84
+ }
85
+ catch (error) {
86
+ throw new Error(`Failed to read SBOM file: ${getErrorMessage(error)}`);
87
+ }
88
+ }
89
+ /**
90
+ * Validates that a directory path exists and is actually a directory
91
+ */
92
+ export function validateDirectory(dirPath) {
93
+ const dir = resolve(dirPath);
94
+ if (!fs.existsSync(dir)) {
95
+ throw new Error(`Directory not found: ${dir}`);
96
+ }
97
+ const stats = fs.statSync(dir);
98
+ if (!stats.isDirectory()) {
99
+ throw new Error(`Path is not a directory: ${dir}`);
100
+ }
101
+ }
102
+ const artifactFilenames = {
103
+ sbom: `${filenamePrefix}.sbom.json`,
104
+ sbomTrimmed: `${filenamePrefix}.sbom-trimmed.json`,
105
+ report: `${filenamePrefix}.report.json`,
106
+ };
107
+ /**
108
+ * Saves an SBOM, trimmed SBOM, or report to disk using the correct default filename.
109
+ */
110
+ export function saveArtifactToFile(dir, request) {
111
+ const defaultFilename = artifactFilenames[request.kind];
112
+ const customOutputPath = 'outputPath' in request ? request.outputPath : undefined;
113
+ const { fileName, fullPath } = resolveOutputPath(dir, defaultFilename, customOutputPath);
114
+ return writeJsonFile(fullPath, fileName, request.payload, fileName);
115
+ }
@@ -5,3 +5,4 @@ import debug from 'debug';
5
5
  * All user-facing output should be handled by commands.
6
6
  */
7
7
  export declare const debugLogger: debug.Debugger;
8
+ export declare function getErrorMessage(error: unknown): string;
@@ -5,3 +5,12 @@ import debug from 'debug';
5
5
  * All user-facing output should be handled by commands.
6
6
  */
7
7
  export const debugLogger = debug('oclif:herodevs-debug');
8
+ export function getErrorMessage(error) {
9
+ if (error instanceof Error) {
10
+ return error.message;
11
+ }
12
+ if (error && typeof error === 'object') {
13
+ return JSON.stringify(error);
14
+ }
15
+ return String(error) || 'Unknown error';
16
+ }
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { createBom } from '@cyclonedx/cdxgen';
4
+ import { filenamePrefix } from "../config/constants.js";
4
5
  import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
5
6
  process.on('uncaughtException', (err) => {
6
7
  console.error('Uncaught exception:', err.message);
@@ -15,7 +16,7 @@ try {
15
16
  const options = JSON.parse(process.argv[2]);
16
17
  const { path, opts } = options;
17
18
  const { bomJson } = await createBom(path, { ...SBOM_DEFAULT__OPTIONS, ...opts });
18
- const outputPath = join(path, 'eol.sbom.json');
19
+ const outputPath = join(path, `${filenamePrefix}.sbom.json`);
19
20
  writeFileSync(outputPath, JSON.stringify(bomJson, null, 2));
20
21
  process.exit(0);
21
22
  }
@@ -0,0 +1 @@
1
+ export declare function stripTypename<T>(obj: T): T;
@@ -0,0 +1,15 @@
1
+ export function stripTypename(obj) {
2
+ if (Array.isArray(obj)) {
3
+ return obj.map(stripTypename);
4
+ }
5
+ if (obj !== null && typeof obj === 'object') {
6
+ const result = {};
7
+ for (const key of Object.keys(obj)) {
8
+ if (key === '__typename')
9
+ continue;
10
+ result[key] = stripTypename(obj[key]);
11
+ }
12
+ return result;
13
+ }
14
+ return obj;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herodevs/cli",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.11",
4
4
  "author": "HeroDevs, Inc",
5
5
  "bin": {
6
6
  "hd": "./bin/run.js"
@@ -17,18 +17,21 @@
17
17
  "ci": "biome ci",
18
18
  "ci:fix": "biome check --write",
19
19
  "clean": "shx rm -rf dist && npm run clean:files && shx rm -rf node_modules",
20
- "clean:files": "shx rm -f eol.**.csv eol.**.json eol.**.text",
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
23
  "format": "biome format --write",
24
24
  "lint": "biome lint --write",
25
25
  "postpack": "shx rm -f oclif.manifest.json",
26
- "prepack": "oclif manifest && oclif readme",
26
+ "prepare": "shx test -d dist || npm run build",
27
+ "prepack": "oclif manifest",
27
28
  "pretest": "npm run lint && npm run typecheck",
28
- "readme": "npm run ci:fix && npm run build && npm exec oclif readme",
29
- "test": "globstar -- node --import tsx --test \"test/**/*.test.ts\"",
29
+ "readme": "npm run ci:fix && npm run build && oclif readme",
30
+ "test": "globstar -- node --import tsx --test --experimental-test-module-mocks \"test/**/*.test.ts\"",
30
31
  "test:e2e": "globstar -- node --import tsx --test \"e2e/**/*.test.ts\"",
31
- "typecheck": "tsc --noEmit"
32
+ "typecheck": "tsc --noEmit",
33
+ "version": "oclif manifest",
34
+ "postversion": "node scripts/update-install-script-version.js && git add README.md"
32
35
  },
33
36
  "keywords": [
34
37
  "herodevs",
@@ -36,30 +39,34 @@
36
39
  "herodevs cli"
37
40
  ],
38
41
  "dependencies": {
42
+ "@amplitude/analytics-node": "^1.5.14",
39
43
  "@apollo/client": "^3.13.8",
40
- "@cyclonedx/cdxgen": "^11.2.7",
41
- "@oclif/core": "^4",
42
- "@oclif/plugin-help": "^6",
43
- "@oclif/plugin-update": "^4",
44
- "@oclif/table": "^0.4.7",
44
+ "@cyclonedx/cdxgen": "~11.4.4",
45
+ "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.11",
46
+ "@oclif/core": "^4.5.3",
47
+ "@oclif/plugin-help": "^6.2.32",
48
+ "@oclif/plugin-update": "^4.7.8",
45
49
  "graphql": "^16.11.0",
50
+ "node-machine-id": "^1.1.12",
51
+ "ora": "^8.2.0",
46
52
  "packageurl-js": "^2.0.1",
53
+ "terminal-link": "^5.0.0",
47
54
  "update-notifier": "^7.3.1"
48
55
  },
49
56
  "devDependencies": {
50
- "@biomejs/biome": "^1.8.3",
51
- "@oclif/test": "^4",
52
- "@types/inquirer": "^9.0.8",
53
- "@types/node": "^22",
57
+ "@biomejs/biome": "^2.2.2",
58
+ "@oclif/test": "^4.1.13",
59
+ "@types/inquirer": "^9.0.9",
60
+ "@types/node": "^24.7.0",
54
61
  "@types/sinon": "^17.0.4",
55
62
  "@types/update-notifier": "^6.0.8",
56
63
  "globstar": "^1.0.0",
57
- "oclif": "^4",
64
+ "oclif": "^4.22.29",
58
65
  "shx": "^0.4.0",
59
- "sinon": "^20.0.0",
60
- "ts-node": "^10",
61
- "tsx": "^4.19.4",
62
- "typescript": "^5.8.3"
66
+ "sinon": "^21.0.0",
67
+ "ts-node": "^10.9.2",
68
+ "tsx": "^4.20.5",
69
+ "typescript": "^5.9.3"
63
70
  },
64
71
  "engines": {
65
72
  "node": ">=20.0.0"
@@ -81,8 +88,12 @@
81
88
  "@oclif/plugin-update"
82
89
  ],
83
90
  "hooks": {
84
- "init": "./dist/hooks/npm-update-notifier.js",
85
- "prerun": "./dist/hooks/prerun.js"
91
+ "init": [
92
+ "./dist/hooks/init/00_npm-update-notifier.js",
93
+ "./dist/hooks/init/01_initialize_amplitude.js"
94
+ ],
95
+ "prerun": "./dist/hooks/prerun/prerun.js",
96
+ "finally": "./dist/hooks/finally/finally.js"
86
97
  },
87
98
  "topicSeparator": " ",
88
99
  "macos": {
@@ -1,12 +0,0 @@
1
- import * as apollo from '@apollo/client/core/index.js';
2
- export interface ApolloHelper {
3
- mutate<T, V extends apollo.OperationVariables>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
4
- query<T, V extends apollo.OperationVariables | undefined = undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
5
- }
6
- export declare const createApollo: (url: string) => apollo.ApolloClient<apollo.NormalizedCacheObject>;
7
- export declare class ApolloClient implements ApolloHelper {
8
- #private;
9
- constructor(url: string);
10
- mutate<T, V extends apollo.OperationVariables>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
11
- query<T, V extends apollo.OperationVariables | undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
12
- }