@herodevs/cli 1.6.0-beta.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,10 @@ The HeroDevs CLI
10
10
  * [@herodevs/cli](#herodevscli)
11
11
  <!-- tocstop -->
12
12
 
13
+ ## TERMS
14
+
15
+ Use of this CLI is governed by the [HeroDevs End of Life Dataset Terms of Service and Data Policy](https://docs.herodevs.com/legal/end-of-life-dataset-terms).
16
+
13
17
  ## Scanning Behavior
14
18
 
15
19
  The CLI's scanning commands (`hd scan eol` and `hd scan sbom`) are designed to be non-invasive:
@@ -26,7 +30,7 @@ $ npm install -g @herodevs/cli
26
30
  $ hd COMMAND
27
31
  running command...
28
32
  $ hd (--version)
29
- @herodevs/cli/1.6.0-beta.0 linux-x64 node-v22.14.0
33
+ @herodevs/cli/2.0.0-beta.1 linux-x64 node-v22.15.0
30
34
  $ hd --help [COMMAND]
31
35
  USAGE
32
36
  $ hd COMMAND
@@ -60,7 +64,7 @@ DESCRIPTION
60
64
  Display help for hd.
61
65
  ```
62
66
 
63
- _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.27/src/commands/help.ts)_
67
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.28/src/commands/help.ts)_
64
68
 
65
69
  ## `hd report committers`
66
70
 
@@ -91,7 +95,7 @@ EXAMPLES
91
95
  $ hd report committers --csv
92
96
  ```
93
97
 
94
- _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.6.0-beta.0/src/commands/report/committers.ts)_
98
+ _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.1/src/commands/report/committers.ts)_
95
99
 
96
100
  ## `hd report purls`
97
101
 
@@ -125,7 +129,7 @@ EXAMPLES
125
129
  $ hd report purls --save --csv
126
130
  ```
127
131
 
128
- _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.6.0-beta.0/src/commands/report/purls.ts)_
132
+ _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.1/src/commands/report/purls.ts)_
129
133
 
130
134
  ## `hd scan eol`
131
135
 
@@ -159,7 +163,7 @@ EXAMPLES
159
163
  $ hd scan eol -a --dir=./my-project
160
164
  ```
161
165
 
162
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.6.0-beta.0/src/commands/scan/eol.ts)_
166
+ _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.1/src/commands/scan/eol.ts)_
163
167
 
164
168
  ## `hd scan sbom`
165
169
 
@@ -187,7 +191,7 @@ EXAMPLES
187
191
  $ hd scan sbom --file=path/to/sbom.json
188
192
  ```
189
193
 
190
- _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.6.0-beta.0/src/commands/scan/sbom.ts)_
194
+ _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.1/src/commands/scan/sbom.ts)_
191
195
 
192
196
  ## `hd update [CHANNEL]`
193
197
 
@@ -225,5 +229,5 @@ EXAMPLES
225
229
  $ hd update --available
226
230
  ```
227
231
 
228
- _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.38/src/commands/update.ts)_
232
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.39/src/commands/update.ts)_
229
233
  <!-- commandsstop -->
package/bin/dev.js CHANGED
@@ -1,13 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Localhost
4
- // process.env.GRAPHQL_HOST = 'http://localhost:3000';
5
-
6
- // Dev
7
3
  process.env.GRAPHQL_HOST = 'https://api.dev.nes.herodevs.com';
8
-
9
- // Prod
10
- // process.env.GRAPHQL_HOST = 'https://api.nes.herodevs.com';
4
+ process.env.EOL_REPORT_URL = 'https://eol-report-card.stage.apps.herodevs.io/reports';
11
5
 
12
6
  import main from './main.js';
13
7
 
@@ -1,4 +1,5 @@
1
1
  import { ApolloClient } from "../../api/client.js";
2
+ import { config } from "../../config/constants.js";
2
3
  import { debugLogger } from "../../service/log.svc.js";
3
4
  import { SbomScanner, buildScanResult } from "../../service/nes/nes.svc.js";
4
5
  import { DEFAULT_SCAN_BATCH_SIZE, DEFAULT_SCAN_INPUT_OPTIONS, } from "../types/hd-cli.types.js";
@@ -21,9 +22,8 @@ export class NesApolloClient {
21
22
  * Submit a scan for a list of purls after they've been batched by batchSubmitPurls
22
23
  */
23
24
  function submitScan(purls, options) {
24
- // NOTE: GRAPHQL_HOST is set in `./bin/dev.js` or tests
25
- const host = process.env.GRAPHQL_HOST || 'https://api.nes.herodevs.com';
26
- const path = process.env.GRAPHQL_PATH || '/graphql';
25
+ const host = config.graphqlHost;
26
+ const path = config.graphqlPath;
27
27
  const url = host + path;
28
28
  const client = new NesApolloClient(url);
29
29
  return client.scan.purls(purls, options);
@@ -38,6 +38,7 @@ export const batchSubmitPurls = async (purls, options = DEFAULT_SCAN_INPUT_OPTIO
38
38
  message: 'No batches to process',
39
39
  success: true,
40
40
  warnings: [],
41
+ scanId: undefined,
41
42
  };
42
43
  }
43
44
  const results = await processBatches(batches, options);
@@ -19,6 +19,7 @@ export interface ScanResult {
19
19
  message: string;
20
20
  success: boolean;
21
21
  warnings: ScanWarning[];
22
+ scanId: string | undefined;
22
23
  }
23
24
  export interface ProcessBatchOptions {
24
25
  batch: string[];
@@ -17,6 +17,7 @@ export default class ScanEol extends Command {
17
17
  }>;
18
18
  private getScan;
19
19
  private getPurlsFromFile;
20
+ private printWebReportUrl;
20
21
  private scanSbom;
21
22
  private getFilteredComponents;
22
23
  private saveReport;
@@ -2,10 +2,11 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command, Flags, ux } from '@oclif/core';
4
4
  import { batchSubmitPurls } from "../../api/nes/nes.client.js";
5
+ import { config } from "../../config/constants.js";
5
6
  import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
6
7
  import { extractPurls, parsePurlsFile } from "../../service/purls.svc.js";
7
8
  import { createStatusDisplay, createTableForStatus, groupComponentsByStatus } from "../../ui/eol.ui.js";
8
- import { INDICATORS, STATUS_COLORS } from "../../ui/shared.ui.js";
9
+ import { INDICATORS, SCAN_ID_KEY, STATUS_COLORS } from "../../ui/shared.ui.js";
9
10
  import ScanSbom from "./sbom.js";
10
11
  export default class ScanEol extends Command {
11
12
  static description = 'Scan a given sbom for EOL data';
@@ -61,6 +62,9 @@ export default class ScanEol extends Command {
61
62
  else {
62
63
  this.displayResults(scan, flags.all);
63
64
  }
65
+ if (scan.scanId) {
66
+ this.printWebReportUrl(scan.scanId);
67
+ }
64
68
  }
65
69
  return { components };
66
70
  }
@@ -82,6 +86,13 @@ export default class ScanEol extends Command {
82
86
  this.error(`Failed to read purls file. ${getErrorMessage(error)}`);
83
87
  }
84
88
  }
89
+ printWebReportUrl(scanId) {
90
+ this.logLine();
91
+ const id = scanId.split(SCAN_ID_KEY)[1];
92
+ const reportCardUrl = config.eolReportUrl;
93
+ const url = ux.colorize('blue', `${reportCardUrl}/${id}`);
94
+ this.log(`🌐 View your free EOL report at: ${ux.colorize('blue', url)}`);
95
+ }
85
96
  async scanSbom(sbom) {
86
97
  let scan;
87
98
  let purls;
@@ -0,0 +1,9 @@
1
+ export declare const EOL_REPORT_URL = "https://eol-report-card.apps.herodevs.com/reports";
2
+ export declare const GRAPHQL_HOST = "https://api.nes.herodevs.com";
3
+ export declare const GRAPHQL_PATH = "/graphql";
4
+ export declare const config: {
5
+ eolReportUrl: string;
6
+ graphqlHost: string;
7
+ graphqlPath: string;
8
+ showVulnCount: boolean;
9
+ };
@@ -0,0 +1,9 @@
1
+ export const EOL_REPORT_URL = 'https://eol-report-card.apps.herodevs.com/reports';
2
+ export const GRAPHQL_HOST = 'https://api.nes.herodevs.com';
3
+ export const GRAPHQL_PATH = '/graphql';
4
+ export const config = {
5
+ eolReportUrl: process.env.EOL_REPORT_URL || EOL_REPORT_URL,
6
+ graphqlHost: process.env.GRAPHQL_HOST || GRAPHQL_HOST,
7
+ graphqlPath: process.env.GRAPHQL_PATH || GRAPHQL_PATH,
8
+ showVulnCount: false,
9
+ };
@@ -2,6 +2,8 @@ import type { Hook } from '@oclif/core';
2
2
  import { type UpdateInfo } from 'update-notifier';
3
3
  declare const updateNotifierHook: Hook.Init;
4
4
  export default updateNotifierHook;
5
+ type DistTag = 'latest' | 'beta' | 'alpha' | 'next';
6
+ export declare function getDistTag(version: string): DistTag;
5
7
  export declare function handleUpdate(update: UpdateInfo, currentVersion: string): {
6
8
  message: string;
7
9
  defer: boolean;
@@ -1,23 +1,43 @@
1
1
  import updateNotifier, {} from 'update-notifier';
2
2
  import pkg from '../../package.json' with { type: 'json' };
3
+ import { debugLogger } from "../service/log.svc.js";
3
4
  const updateNotifierHook = async (options) => {
5
+ debugLogger('pkg.version', pkg.version);
6
+ const distTag = getDistTag(pkg.version);
7
+ debugLogger('distTag', distTag);
8
+ const ONE_DAY_MS = 1000 * 60 * 60 * 24;
9
+ // If we're on the latest dist-tag, check for updates every time
10
+ const updateCheckInterval = distTag === 'latest' ? 0 : ONE_DAY_MS;
11
+ debugLogger('updateCheckInterval', updateCheckInterval);
4
12
  const notifier = updateNotifier({
5
13
  pkg,
6
- updateCheckInterval: 1000 * 60 * 60 * 24, // Check once per day
14
+ distTag,
15
+ updateCheckInterval,
16
+ shouldNotifyInNpmScript: true,
7
17
  });
18
+ debugLogger('updateNotifierHook', { notifier });
8
19
  if (notifier.update) {
9
20
  const notification = handleUpdate(notifier.update, pkg.version);
21
+ debugLogger('notification', notification);
10
22
  if (notification) {
11
23
  notifier.notify(notification);
12
24
  }
13
25
  }
14
26
  };
15
27
  export default updateNotifierHook;
28
+ export function getDistTag(version) {
29
+ if (version.includes('-beta'))
30
+ return 'beta';
31
+ if (version.includes('-alpha'))
32
+ return 'alpha';
33
+ if (version.includes('-next'))
34
+ return 'next';
35
+ return 'latest';
36
+ }
16
37
  export function handleUpdate(update, currentVersion) {
17
- const isPreV1 = currentVersion.startsWith('0.');
18
- const isBeta = currentVersion.includes('-beta') || update.latest.includes('-beta');
19
- const isAlpha = currentVersion.includes('-alpha') || update.latest.includes('-alpha');
20
- const isNext = currentVersion.includes('-next') || update.latest.includes('-next');
38
+ const isPreV1 = currentVersion.startsWith('0.') || update.latest.startsWith('0.');
39
+ const currentDistTag = getDistTag(currentVersion);
40
+ const updateDistTag = getDistTag(update.latest);
21
41
  let message = `Update available! v${currentVersion} → v${update.latest}`;
22
42
  /**
23
43
  * Show breaking changes warning for:
@@ -28,7 +48,7 @@ export function handleUpdate(update, currentVersion) {
28
48
  * [1]https://semver.org/#spec-item-4
29
49
  * [2]https://antfu.me/posts/epoch-semver#leading-zero-major-versioning
30
50
  */
31
- if (isPreV1 || isBeta || isAlpha || isNext || update.type === 'major') {
51
+ if (isPreV1 || currentDistTag !== 'latest' || updateDistTag !== 'latest' || update.type === 'major') {
32
52
  message += '\nThis update may contain breaking changes.';
33
53
  }
34
54
  // For all other updates (minor, patch), they should be non-breaking
@@ -10,6 +10,7 @@ export const buildScanResult = (scan) => {
10
10
  message: scan.message,
11
11
  success: true,
12
12
  warnings: scan.warnings || [],
13
+ scanId: scan.scanId,
13
14
  };
14
15
  };
15
16
  export const SbomScanner = (client) => async (purls, options) => {
package/dist/ui/eol.ui.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ux } from '@oclif/core';
2
2
  import { makeTable } from '@oclif/table';
3
3
  import { PackageURL } from 'packageurl-js';
4
+ import { config } from "../config/constants.js";
4
5
  import { resolvePurlPackageName } from "../service/eol/eol.svc.js";
5
6
  import { parseMomentToSimpleDate } from "./date.ui.js";
6
7
  import { INDICATORS, MAX_PURL_LENGTH, MAX_TABLE_COLUMN_WIDTH, STATUS_COLORS } from "./shared.ui.js";
@@ -19,10 +20,10 @@ function getDaysEolString(daysEol) {
19
20
  if (daysEol === null) {
20
21
  return '';
21
22
  }
22
- if (daysEol < 0) {
23
- return `${Math.abs(daysEol)} days from now`;
23
+ if (daysEol <= 0) {
24
+ return `${Math.abs(daysEol) + 1} days from now`;
24
25
  }
25
- if (daysEol === 0) {
26
+ if (daysEol > 0) {
26
27
  return 'today';
27
28
  }
28
29
  return `${daysEol} days ago`;
@@ -32,13 +33,11 @@ function formatDetailedComponent(purl, info) {
32
33
  const simpleComponent = formatSimpleComponent(purl, status);
33
34
  const eolAtString = parseMomentToSimpleDate(eolAt);
34
35
  const daysEolString = getDaysEolString(daysEol);
35
- const output = [
36
- `${simpleComponent}`,
37
- ` ⮑ EOL Date: ${eolAtString} (${daysEolString})`,
38
- ` ⮑ # of Vulns: ${vulnCount ?? ''}`,
39
- ]
40
- .filter(Boolean)
41
- .join('\n');
36
+ const eolString = [`${simpleComponent}`, ` ⮑ EOL Date: ${eolAtString} (${daysEolString})`];
37
+ if (config.showVulnCount) {
38
+ eolString.push(` ⮑ # of Vulns: ${vulnCount ?? ''}`);
39
+ }
40
+ const output = eolString.filter(Boolean).join('\n');
42
41
  return output;
43
42
  }
44
43
  export function createStatusDisplay(components, all) {
@@ -65,6 +64,19 @@ export function createStatusDisplay(components, all) {
65
64
  export function createTableForStatus(grouped, status) {
66
65
  const data = grouped[status].map((component) => convertComponentToTableRow(component));
67
66
  if (status === 'EOL' || status === 'SUPPORTED') {
67
+ if (config.showVulnCount) {
68
+ return makeTable({
69
+ data,
70
+ columns: [
71
+ { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
72
+ { key: 'version', name: 'VERSION', width: 10 },
73
+ { key: 'eol', name: 'EOL', width: 12 },
74
+ { key: 'daysEol', name: 'DAYS EOL', width: 10 },
75
+ { key: 'type', name: 'TYPE', width: 12 },
76
+ { key: 'vulnCount', name: '# OF VULNS', width: 12 },
77
+ ],
78
+ });
79
+ }
68
80
  return makeTable({
69
81
  data,
70
82
  columns: [
@@ -73,6 +85,16 @@ export function createTableForStatus(grouped, status) {
73
85
  { key: 'eol', name: 'EOL', width: 12 },
74
86
  { key: 'daysEol', name: 'DAYS EOL', width: 10 },
75
87
  { key: 'type', name: 'TYPE', width: 12 },
88
+ ],
89
+ });
90
+ }
91
+ if (config.showVulnCount) {
92
+ return makeTable({
93
+ data,
94
+ columns: [
95
+ { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
96
+ { key: 'version', name: 'VERSION', width: 10 },
97
+ { key: 'type', name: 'TYPE', width: 12 },
76
98
  { key: 'vulnCount', name: '# OF VULNS', width: 12 },
77
99
  ],
78
100
  });
@@ -83,7 +105,6 @@ export function createTableForStatus(grouped, status) {
83
105
  { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
84
106
  { key: 'version', name: 'VERSION', width: 10 },
85
107
  { key: 'type', name: 'TYPE', width: 12 },
86
- { key: 'vulnCount', name: '# OF VULNS', width: 12 },
87
108
  ],
88
109
  });
89
110
  }
@@ -3,3 +3,4 @@ export declare const STATUS_COLORS: Record<ComponentStatus, string>;
3
3
  export declare const INDICATORS: Record<ComponentStatus, string>;
4
4
  export declare const MAX_PURL_LENGTH = 60;
5
5
  export declare const MAX_TABLE_COLUMN_WIDTH = 30;
6
+ export declare const SCAN_ID_KEY = "eol-scan-v1-";
@@ -13,3 +13,4 @@ export const INDICATORS = {
13
13
  };
14
14
  export const MAX_PURL_LENGTH = 60;
15
15
  export const MAX_TABLE_COLUMN_WIDTH = 30;
16
+ export const SCAN_ID_KEY = 'eol-scan-v1-';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herodevs/cli",
3
- "version": "1.6.0-beta.0",
3
+ "version": "2.0.0-beta.1",
4
4
  "author": "HeroDevs, Inc",
5
5
  "bin": {
6
6
  "hd": "./bin/run.js"
@@ -37,19 +37,19 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@apollo/client": "^3.13.8",
40
- "@cyclonedx/cdxgen": "^11.2.5",
40
+ "@cyclonedx/cdxgen": "^11.2.7",
41
41
  "@oclif/core": "^4",
42
42
  "@oclif/plugin-help": "^6",
43
43
  "@oclif/plugin-update": "^4",
44
44
  "@oclif/table": "^0.4.7",
45
- "graphql": "^16.8.1",
45
+ "graphql": "^16.11.0",
46
46
  "packageurl-js": "^2.0.1",
47
47
  "update-notifier": "^7.3.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@biomejs/biome": "^1.8.3",
51
51
  "@oclif/test": "^4",
52
- "@types/inquirer": "^9.0.7",
52
+ "@types/inquirer": "^9.0.8",
53
53
  "@types/node": "^22",
54
54
  "@types/sinon": "^17.0.4",
55
55
  "@types/update-notifier": "^6.0.8",
@@ -58,7 +58,7 @@
58
58
  "shx": "^0.4.0",
59
59
  "sinon": "^20.0.0",
60
60
  "ts-node": "^10",
61
- "tsx": "^4.19.3",
61
+ "tsx": "^4.19.4",
62
62
  "typescript": "^5.8.3"
63
63
  },
64
64
  "engines": {