@herodevs/cli 1.4.0-beta.1 → 1.5.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
@@ -16,7 +16,7 @@ $ npm install -g @herodevs/cli
16
16
  $ hd COMMAND
17
17
  running command...
18
18
  $ hd (--version)
19
- @herodevs/cli/1.4.0-beta.1 linux-x64 node-v22.14.0
19
+ @herodevs/cli/1.5.0-beta.1 linux-x64 node-v22.14.0
20
20
  $ hd --help [COMMAND]
21
21
  USAGE
22
22
  $ hd COMMAND
@@ -81,7 +81,7 @@ EXAMPLES
81
81
  $ hd report committers --csv
82
82
  ```
83
83
 
84
- _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.4.0-beta.1/src/commands/report/committers.ts)_
84
+ _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.5.0-beta.1/src/commands/report/committers.ts)_
85
85
 
86
86
  ## `hd report purls`
87
87
 
@@ -115,7 +115,7 @@ EXAMPLES
115
115
  $ hd report purls --save --csv
116
116
  ```
117
117
 
118
- _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.4.0-beta.1/src/commands/report/purls.ts)_
118
+ _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.5.0-beta.1/src/commands/report/purls.ts)_
119
119
 
120
120
  ## `hd scan eol`
121
121
 
@@ -123,15 +123,15 @@ Scan a given sbom for EOL data
123
123
 
124
124
  ```
125
125
  USAGE
126
- $ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s] [-a] [-c]
126
+ $ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s] [-a] [-t]
127
127
 
128
128
  FLAGS
129
- -a, --all Show all components (default is EOL and LTS only)
130
- -c, --getCustomerSupport Get Never-Ending Support for End-of-Life components
131
- -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
132
- -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
133
- -p, --purls=<value> The file path of a list of purls to scan for EOL
134
- -s, --save Save the generated SBOM as nes.sbom.json in the scanned directory
129
+ -a, --all Show all components (default is EOL and SUPPORTED only)
130
+ -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
131
+ -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
132
+ -p, --purls=<value> The file path of a list of purls to scan for EOL
133
+ -s, --save Save the generated SBOM as nes.sbom.json in the scanned directory
134
+ -t, --table Display the results in a table
135
135
 
136
136
  GLOBAL FLAGS
137
137
  --json Format output as json.
@@ -149,7 +149,7 @@ EXAMPLES
149
149
  $ hd scan eol -a --dir=./my-project
150
150
  ```
151
151
 
152
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.4.0-beta.1/src/commands/scan/eol.ts)_
152
+ _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.5.0-beta.1/src/commands/scan/eol.ts)_
153
153
 
154
154
  ## `hd scan sbom`
155
155
 
@@ -177,7 +177,7 @@ EXAMPLES
177
177
  $ hd scan sbom --file=path/to/sbom.json
178
178
  ```
179
179
 
180
- _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.4.0-beta.1/src/commands/scan/sbom.ts)_
180
+ _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.5.0-beta.1/src/commands/scan/sbom.ts)_
181
181
 
182
182
  ## `hd update [CHANNEL]`
183
183
 
@@ -215,5 +215,5 @@ EXAMPLES
215
215
  $ hd update --available
216
216
  ```
217
217
 
218
- _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.36/src/commands/update.ts)_
218
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.37/src/commands/update.ts)_
219
219
  <!-- commandsstop -->
@@ -1,6 +1,6 @@
1
1
  import type * as apollo from '@apollo/client/core/index.js';
2
2
  import type { InsightsEolScanInput, InsightsEolScanResult } from '../../api/types/nes.types.ts';
3
- import type { ProcessBatchOptions, ScanInputOptions, ScanResult } from '../types/hd-cli.types.ts';
3
+ import { type ProcessBatchOptions, type ScanInputOptions, type ScanResult } from '../types/hd-cli.types.ts';
4
4
  export interface NesClient {
5
5
  scan: {
6
6
  purls: (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
@@ -15,7 +15,7 @@ export declare class NesApolloClient implements NesClient {
15
15
  mutate<T, V extends Record<string, unknown>>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
16
16
  query<T, V extends Record<string, unknown> | undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
17
17
  }
18
- export declare const batchSubmitPurls: (purls: string[], options: ScanInputOptions, batchSize: number) => Promise<ScanResult>;
18
+ export declare const batchSubmitPurls: (purls: string[], options?: ScanInputOptions, batchSize?: number) => Promise<ScanResult>;
19
19
  export declare const createBatches: (items: string[], batchSize: number) => string[][];
20
20
  export declare const processBatch: ({ batch, index, totalPages, scanOptions, previousScanId, }: ProcessBatchOptions) => Promise<InsightsEolScanResult>;
21
21
  export declare const processBatches: (batches: string[][], scanOptions: ScanInputOptions) => Promise<InsightsEolScanResult[]>;
@@ -1,6 +1,7 @@
1
1
  import { ApolloClient } from "../../api/client.js";
2
2
  import { debugLogger } from "../../service/log.svc.js";
3
3
  import { SbomScanner, buildScanResult } from "../../service/nes/nes.svc.js";
4
+ import { DEFAULT_SCAN_BATCH_SIZE, DEFAULT_SCAN_INPUT_OPTIONS, } from "../types/hd-cli.types.js";
4
5
  export class NesApolloClient {
5
6
  scan = {
6
7
  purls: SbomScanner(this),
@@ -27,7 +28,7 @@ function submitScan(purls, options) {
27
28
  const client = new NesApolloClient(url);
28
29
  return client.scan.purls(purls, options);
29
30
  }
30
- export const batchSubmitPurls = async (purls, options, batchSize) => {
31
+ export const batchSubmitPurls = async (purls, options = DEFAULT_SCAN_INPUT_OPTIONS, batchSize = DEFAULT_SCAN_BATCH_SIZE) => {
31
32
  try {
32
33
  const batches = createBatches(purls, batchSize);
33
34
  debugLogger('Processing %d batches', batches.length);
@@ -61,6 +62,7 @@ export const processBatch = async ({ batch, index, totalPages, scanOptions, prev
61
62
  throw new Error('Total pages exceeded');
62
63
  }
63
64
  debugLogger('Processing batch %d of %d', page, totalPages);
65
+ debugLogger('ScanID: %s', previousScanId);
64
66
  const result = await submitScan(batch, {
65
67
  ...scanOptions,
66
68
  page,
@@ -2,9 +2,9 @@ import { gql } from '@apollo/client/core/core.cjs';
2
2
  export const M_SCAN = {
3
3
  gql: gql `
4
4
  mutation EolScan($input: InsightsEolScanInput!) {
5
- insights {
6
- scan {
7
- eol(input: $input) {
5
+ insights {
6
+ scan {
7
+ eol(input: $input) {
8
8
  components {
9
9
  purl
10
10
  info {
@@ -13,8 +13,10 @@ export const M_SCAN = {
13
13
  eolAt
14
14
  daysEol
15
15
  status
16
+ # TODO: uncomment vulnCount once backend changes are deployed
17
+ # vulnCount
16
18
  }
17
- }
19
+ }
18
20
  diagnostics
19
21
  message
20
22
  scanId
@@ -36,6 +36,7 @@ export interface InsightsEolScanComponentInfo {
36
36
  eolAt: Date | null;
37
37
  status: ComponentStatus;
38
38
  daysEol: number | null;
39
+ vulnCount: number | null;
39
40
  }
40
41
  export interface InsightsEolScanComponent {
41
42
  info: InsightsEolScanComponentInfo;
@@ -49,4 +50,4 @@ export interface ScanWarning {
49
50
  diagnostics?: Record<string, unknown>;
50
51
  }
51
52
  export type ComponentStatus = (typeof VALID_STATUSES)[number];
52
- export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "LTS"];
53
+ export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "SUPPORTED"];
@@ -1 +1 @@
1
- export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', 'LTS'];
1
+ export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', 'SUPPORTED'];
@@ -10,7 +10,7 @@ export default class ScanEol extends Command {
10
10
  dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
- getCustomerSupport: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
14
  };
15
15
  run(): Promise<{
16
16
  components: InsightsEolScanComponent[];
@@ -21,6 +21,8 @@ export default class ScanEol extends Command {
21
21
  private getFilteredComponents;
22
22
  private saveReport;
23
23
  private displayResults;
24
+ private displayResultsInTable;
25
+ private displayTable;
24
26
  private displayNoComponentsMessage;
25
27
  private logLine;
26
28
  private displayStatusSection;
@@ -2,12 +2,10 @@ 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 { DEFAULT_SCAN_BATCH_SIZE, DEFAULT_SCAN_INPUT_OPTIONS } from '../../api/types/hd-cli.types.js';
6
5
  import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
7
- import { extractPurls } from "../../service/purls.svc.js";
8
- import { parsePurlsFile } from "../../service/purls.svc.js";
9
- import { createStatusDisplay } from "../../ui/eol.ui.js";
10
- import { INDICATORS, STATUS_COLORS } from "../../ui/shared.us.js";
6
+ import { extractPurls, parsePurlsFile } from "../../service/purls.svc.js";
7
+ import { createStatusDisplay, createTableForStatus, groupComponentsByStatus } from "../../ui/eol.ui.js";
8
+ import { INDICATORS, STATUS_COLORS } from "../../ui/shared.ui.js";
11
9
  import ScanSbom from "./sbom.js";
12
10
  export default class ScanEol extends Command {
13
11
  static description = 'Scan a given sbom for EOL data';
@@ -38,46 +36,44 @@ export default class ScanEol extends Command {
38
36
  }),
39
37
  all: Flags.boolean({
40
38
  char: 'a',
41
- description: 'Show all components (default is EOL and LTS only)',
39
+ description: 'Show all components (default is EOL and SUPPORTED only)',
42
40
  default: false,
43
41
  }),
44
- getCustomerSupport: Flags.boolean({
45
- char: 'c',
46
- description: 'Get Never-Ending Support for End-of-Life components',
42
+ table: Flags.boolean({
43
+ char: 't',
44
+ description: 'Display the results in a table',
47
45
  default: false,
48
46
  }),
49
47
  };
50
48
  async run() {
51
49
  const { flags } = await this.parse(ScanEol);
52
- if (flags.getCustomerSupport) {
53
- this.log(ux.colorize('yellow', 'Never-Ending Support is on the way. Please stay tuned for this feature.'));
54
- }
55
50
  const scan = await this.getScan(flags, this.config);
56
51
  ux.action.stop('\nScan completed');
57
- const filteredComponents = this.getFilteredComponents(scan, flags.all);
52
+ const components = this.getFilteredComponents(scan, flags.all);
58
53
  if (flags.save) {
59
- await this.saveReport(filteredComponents);
54
+ await this.saveReport(components);
60
55
  }
61
- if (this.jsonEnabled()) {
62
- return { components: filteredComponents };
56
+ if (!this.jsonEnabled()) {
57
+ if (flags.table) {
58
+ this.log(`${scan.components.size} components scanned`);
59
+ this.displayResultsInTable(scan, flags.all);
60
+ }
61
+ else {
62
+ this.displayResults(scan, flags.all);
63
+ }
63
64
  }
64
- await this.displayResults(scan, flags.all);
65
- return { components: filteredComponents };
65
+ return { components };
66
66
  }
67
67
  async getScan(flags, config) {
68
68
  if (flags.purls) {
69
69
  ux.action.start(`Scanning purls from ${flags.purls}`);
70
70
  const purls = this.getPurlsFromFile(flags.purls);
71
- return batchSubmitPurls(purls, DEFAULT_SCAN_INPUT_OPTIONS, DEFAULT_SCAN_BATCH_SIZE);
71
+ return batchSubmitPurls(purls);
72
72
  }
73
73
  const sbom = await ScanSbom.loadSbom(flags, config);
74
- const scan = this.scanSbom(sbom, flags);
75
- return scan;
74
+ return this.scanSbom(sbom);
76
75
  }
77
76
  getPurlsFromFile(filePath) {
78
- if (typeof filePath !== 'string') {
79
- this.error(`Failed to parse file path: ${filePath}`);
80
- }
81
77
  try {
82
78
  const purlsFileString = fs.readFileSync(filePath, 'utf8');
83
79
  return parsePurlsFile(purlsFileString);
@@ -86,7 +82,7 @@ export default class ScanEol extends Command {
86
82
  this.error(`Failed to read purls file. ${getErrorMessage(error)}`);
87
83
  }
88
84
  }
89
- async scanSbom(sbom, flags) {
85
+ async scanSbom(sbom) {
90
86
  let scan;
91
87
  let purls;
92
88
  try {
@@ -96,7 +92,7 @@ export default class ScanEol extends Command {
96
92
  this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`);
97
93
  }
98
94
  try {
99
- scan = await batchSubmitPurls(purls, DEFAULT_SCAN_INPUT_OPTIONS, DEFAULT_SCAN_BATCH_SIZE);
95
+ scan = await batchSubmitPurls(purls);
100
96
  }
101
97
  catch (error) {
102
98
  this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
@@ -107,52 +103,67 @@ export default class ScanEol extends Command {
107
103
  return scan;
108
104
  }
109
105
  getFilteredComponents(scan, all) {
110
- return Array.from(scan.components.entries())
111
- .filter(([_, component]) => all || ['EOL', 'LTS'].includes(component.info.status))
112
- .map(([_, component]) => component);
106
+ return Array.from(scan.components.values()).filter((component) => all || ['EOL', 'SUPPORTED'].includes(component.info.status));
113
107
  }
114
108
  async saveReport(components) {
109
+ const { flags } = await this.parse(ScanEol);
110
+ const reportPath = path.join(flags.dir || process.cwd(), 'nes.eol.json');
115
111
  try {
116
- const { flags } = await this.parse(ScanEol);
117
- const reportPath = path.join(flags.dir || process.cwd(), 'nes.eol.json');
118
112
  fs.writeFileSync(reportPath, JSON.stringify({ components }, null, 2));
119
113
  this.log('Report saved to nes.eol.json');
120
114
  }
121
115
  catch (error) {
122
- if (isErrnoException(error)) {
123
- switch (error.code) {
124
- case 'EACCES':
125
- this.error('Permission denied. Unable to save report to nes.eol.json');
126
- break;
127
- case 'ENOSPC':
128
- this.error('No space left on device. Unable to save report to nes.eol.json');
129
- break;
130
- default:
131
- this.error(`Failed to save report: ${getErrorMessage(error)}`);
132
- }
133
- }
134
- else {
116
+ if (!isErrnoException(error)) {
135
117
  this.error(`Failed to save report: ${getErrorMessage(error)}`);
136
118
  }
119
+ switch (error.code) {
120
+ case 'EACCES':
121
+ this.error('Permission denied. Unable to save report to nes.eol.json');
122
+ break;
123
+ case 'ENOSPC':
124
+ this.error('No space left on device. Unable to save report to nes.eol.json');
125
+ break;
126
+ default:
127
+ this.error(`Failed to save report: ${getErrorMessage(error)}`);
128
+ }
137
129
  }
138
130
  }
139
- async displayResults(scan, all) {
140
- const { UNKNOWN, OK, LTS, EOL } = createStatusDisplay(scan.components, all);
141
- if (!UNKNOWN.length && !OK.length && !LTS.length && !EOL.length) {
131
+ displayResults(scan, all) {
132
+ const { UNKNOWN, OK, SUPPORTED, EOL } = createStatusDisplay(scan.components, all);
133
+ if (!UNKNOWN.length && !OK.length && !SUPPORTED.length && !EOL.length) {
142
134
  this.displayNoComponentsMessage(all);
143
135
  return;
144
136
  }
145
137
  this.log(ux.colorize('bold', 'Here are the results of the scan:'));
146
138
  this.logLine();
147
139
  // Display sections in order of increasing severity
148
- for (const components of [UNKNOWN, OK, LTS, EOL]) {
140
+ for (const components of [UNKNOWN, OK, SUPPORTED, EOL]) {
149
141
  this.displayStatusSection(components);
150
142
  }
151
143
  this.logLegend();
152
144
  }
145
+ displayResultsInTable(scan, all) {
146
+ const grouped = groupComponentsByStatus(scan.components);
147
+ const statuses = ['SUPPORTED', 'EOL'];
148
+ if (all) {
149
+ statuses.unshift('UNKNOWN', 'OK');
150
+ }
151
+ for (const status of statuses) {
152
+ const components = grouped[status];
153
+ if (components.length > 0) {
154
+ const table = createTableForStatus(grouped, status);
155
+ this.displayTable(table, components.length, status);
156
+ }
157
+ }
158
+ this.logLegend();
159
+ }
160
+ displayTable(table, count, status) {
161
+ this.log(ux.colorize(STATUS_COLORS[status], `${INDICATORS[status]} ${count} ${status} Component(s):`));
162
+ this.log(ux.colorize(STATUS_COLORS[status], table));
163
+ }
153
164
  displayNoComponentsMessage(all) {
154
165
  if (!all) {
155
- this.log(ux.colorize('yellow', 'No End-of-Life or Long Term Support components found in scan.'));
166
+ this.log(ux.colorize('yellow', 'No End-of-Life or Supported components found in scan.'));
156
167
  this.log(ux.colorize('yellow', 'Use --all flag to view all components.'));
157
168
  }
158
169
  else {
@@ -169,9 +180,9 @@ export default class ScanEol extends Command {
169
180
  }
170
181
  }
171
182
  logLegend() {
172
- this.log(ux.colorize(`${STATUS_COLORS.UNKNOWN}`, `${INDICATORS.UNKNOWN} = No Known Issues`));
173
- this.log(ux.colorize(`${STATUS_COLORS.OK}`, `${INDICATORS.OK} = OK`));
174
- this.log(ux.colorize(`${STATUS_COLORS.LTS}`, `${INDICATORS.LTS}= Long Term Support (LTS)`));
175
- this.log(ux.colorize(`${STATUS_COLORS.EOL}`, `${INDICATORS.EOL} = End of Life (EOL)`));
183
+ this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} = No Known Issues`));
184
+ this.log(ux.colorize(STATUS_COLORS.OK, `${INDICATORS.OK} = OK`));
185
+ this.log(ux.colorize(STATUS_COLORS.SUPPORTED, `${INDICATORS.SUPPORTED}= Supported: End-of-Life (EOL) is scheduled`));
186
+ this.log(ux.colorize(STATUS_COLORS.EOL, `${INDICATORS.EOL} = End of Life (EOL)`));
176
187
  }
177
188
  }
@@ -0,0 +1,8 @@
1
+ import type { Hook } from '@oclif/core';
2
+ import { type UpdateInfo } from 'update-notifier';
3
+ declare const updateNotifierHook: Hook.Init;
4
+ export default updateNotifierHook;
5
+ export declare function handleUpdate(update: UpdateInfo, currentVersion: string): {
6
+ message: string;
7
+ defer: boolean;
8
+ };
@@ -0,0 +1,39 @@
1
+ import updateNotifier, {} from 'update-notifier';
2
+ import pkg from '../../package.json' with { type: 'json' };
3
+ const updateNotifierHook = async (options) => {
4
+ const notifier = updateNotifier({
5
+ pkg,
6
+ updateCheckInterval: 1000 * 60 * 60 * 24, // Check once per day
7
+ });
8
+ if (notifier.update) {
9
+ const notification = handleUpdate(notifier.update, pkg.version);
10
+ if (notification) {
11
+ notifier.notify(notification);
12
+ }
13
+ }
14
+ };
15
+ export default updateNotifierHook;
16
+ 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');
21
+ let message = `Update available! v${currentVersion} → v${update.latest}`;
22
+ /**
23
+ * Show breaking changes warning for:
24
+ * - v0.x.x versions (all updates can contain breaking changes per SemVer spec[1][2])
25
+ * - Prerelease versions (beta/alpha/next)
26
+ * - Major version updates
27
+ *
28
+ * [1]https://semver.org/#spec-item-4
29
+ * [2]https://antfu.me/posts/epoch-semver#leading-zero-major-versioning
30
+ */
31
+ if (isPreV1 || isBeta || isAlpha || isNext || update.type === 'major') {
32
+ message += '\nThis update may contain breaking changes.';
33
+ }
34
+ // For all other updates (minor, patch), they should be non-breaking
35
+ return {
36
+ message,
37
+ defer: false,
38
+ };
39
+ }
@@ -3,6 +3,12 @@ import { debugLogger } from "../log.svc.js";
3
3
  export const buildScanResult = (scan) => {
4
4
  const components = new Map();
5
5
  for (const c of scan.components) {
6
+ const { status } = c.info;
7
+ // TODO: remove this once backend changes are deployed
8
+ // @ts-expect-error
9
+ if (status === 'LTS') {
10
+ c.info.status = 'SUPPORTED';
11
+ }
6
12
  components.set(c.purl, c);
7
13
  }
8
14
  return {
@@ -1,5 +1,15 @@
1
1
  import type { ScanResultComponentsMap } from '../api/types/hd-cli.types.ts';
2
- import type { ComponentStatus } from '../api/types/nes.types.ts';
3
- export declare function truncatePurl(purl: string): string;
2
+ import type { ComponentStatus, InsightsEolScanComponent } from '../api/types/nes.types.ts';
3
+ export declare function truncateString(purl: string, maxLength: number): string;
4
4
  export declare function colorizeStatus(status: ComponentStatus): string;
5
5
  export declare function createStatusDisplay(components: ScanResultComponentsMap, all: boolean): Record<ComponentStatus, string[]>;
6
+ export declare function createTableForStatus(grouped: Record<ComponentStatus, InsightsEolScanComponent[]>, status: ComponentStatus): string;
7
+ export declare function convertComponentToTableRow(component: InsightsEolScanComponent): {
8
+ name: string;
9
+ version: string;
10
+ eol: string;
11
+ daysEol: number | null;
12
+ type: string;
13
+ vulnCount: number | null;
14
+ };
15
+ export declare function groupComponentsByStatus(components: ScanResultComponentsMap): Record<ComponentStatus, InsightsEolScanComponent[]>;
package/dist/ui/eol.ui.js CHANGED
@@ -1,36 +1,41 @@
1
1
  import { ux } from '@oclif/core';
2
+ import { makeTable } from '@oclif/table';
3
+ import { PackageURL } from 'packageurl-js';
2
4
  import { parseMomentToSimpleDate } from "./date.ui.js";
3
- import { INDICATORS, STATUS_COLORS } from "./shared.us.js";
4
- export function truncatePurl(purl) {
5
- return purl.length > 60 ? `${purl.slice(0, 57)}...` : purl;
5
+ import { INDICATORS, MAX_PURL_LENGTH, MAX_TABLE_COLUMN_WIDTH, STATUS_COLORS } from "./shared.ui.js";
6
+ export function truncateString(purl, maxLength) {
7
+ const ellipses = '...';
8
+ return purl.length > maxLength ? `${purl.slice(0, maxLength - ellipses.length)}${ellipses}` : purl;
6
9
  }
7
10
  export function colorizeStatus(status) {
8
11
  return ux.colorize(STATUS_COLORS[status], status);
9
12
  }
10
13
  function formatSimpleComponent(purl, status) {
11
14
  const color = STATUS_COLORS[status];
12
- return ` ${INDICATORS[status]} ${ux.colorize(color, truncatePurl(purl))}`;
15
+ return ` ${INDICATORS[status]} ${ux.colorize(color, truncateString(purl, MAX_PURL_LENGTH))}`;
13
16
  }
14
17
  function getDaysEolString(daysEol) {
15
- // UNKNOWN || OK
16
18
  if (daysEol === null) {
17
19
  return '';
18
20
  }
19
- // LTS
20
21
  if (daysEol < 0) {
21
22
  return `${Math.abs(daysEol)} days from now`;
22
23
  }
23
- // EOL
24
24
  if (daysEol === 0) {
25
25
  return 'today';
26
26
  }
27
27
  return `${daysEol} days ago`;
28
28
  }
29
- function formatDetailedComponent(purl, eolAt, daysEol, status) {
29
+ function formatDetailedComponent(purl, info) {
30
+ const { status, eolAt, daysEol, vulnCount } = info;
30
31
  const simpleComponent = formatSimpleComponent(purl, status);
31
32
  const eolAtString = parseMomentToSimpleDate(eolAt);
32
33
  const daysEolString = getDaysEolString(daysEol);
33
- const output = [`${simpleComponent}`, ` ⮑ EOL Date: ${eolAtString} (${daysEolString})`]
34
+ const output = [
35
+ `${simpleComponent}`,
36
+ ` ⮑ EOL Date: ${eolAtString} (${daysEolString})`,
37
+ ` ⮑ # of Vulns: ${vulnCount ?? ''}`,
38
+ ]
34
39
  .filter(Boolean)
35
40
  .join('\n');
36
41
  return output;
@@ -39,20 +44,58 @@ export function createStatusDisplay(components, all) {
39
44
  const statusOutput = {
40
45
  UNKNOWN: [],
41
46
  OK: [],
42
- LTS: [],
47
+ SUPPORTED: [],
43
48
  EOL: [],
44
49
  };
45
50
  // Single loop to separate and format components
46
51
  for (const [purl, component] of components.entries()) {
47
- const { status, eolAt, daysEol } = component.info;
52
+ const { status } = component.info;
48
53
  if (all) {
49
54
  if (status === 'UNKNOWN' || status === 'OK') {
50
55
  statusOutput[status].push(formatSimpleComponent(purl, status));
51
56
  }
52
57
  }
53
- if (status === 'LTS' || status === 'EOL') {
54
- statusOutput[status].push(formatDetailedComponent(purl, eolAt, daysEol, status));
58
+ if (status === 'SUPPORTED' || status === 'EOL') {
59
+ statusOutput[status].push(formatDetailedComponent(purl, component.info));
55
60
  }
56
61
  }
57
62
  return statusOutput;
58
63
  }
64
+ export function createTableForStatus(grouped, status) {
65
+ const data = grouped[status].map((component) => convertComponentToTableRow(component));
66
+ return makeTable({
67
+ data,
68
+ columns: [
69
+ { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
70
+ { key: 'version', name: 'VERSION', width: 10 },
71
+ { key: 'eol', name: 'EOL', width: 12 },
72
+ { key: 'daysEol', name: 'DAYS EOL', width: 10 },
73
+ { key: 'type', name: 'TYPE', width: 12 },
74
+ { key: 'vulnCount', name: '# OF VULNS', width: 12 },
75
+ ],
76
+ });
77
+ }
78
+ export function convertComponentToTableRow(component) {
79
+ const purlParts = PackageURL.fromString(component.purl);
80
+ const { eolAt, daysEol, vulnCount } = component.info;
81
+ return {
82
+ name: purlParts.name,
83
+ version: purlParts.version ?? '',
84
+ eol: parseMomentToSimpleDate(eolAt),
85
+ daysEol: daysEol,
86
+ type: purlParts.type,
87
+ vulnCount: vulnCount,
88
+ };
89
+ }
90
+ export function groupComponentsByStatus(components) {
91
+ const grouped = {
92
+ UNKNOWN: [],
93
+ OK: [],
94
+ SUPPORTED: [],
95
+ EOL: [],
96
+ };
97
+ for (const component of components.values()) {
98
+ grouped[component.info.status].push(component);
99
+ }
100
+ return grouped;
101
+ }
@@ -1,3 +1,5 @@
1
1
  import type { ComponentStatus } from '../api/types/nes.types.ts';
2
2
  export declare const STATUS_COLORS: Record<ComponentStatus, string>;
3
3
  export declare const INDICATORS: Record<ComponentStatus, string>;
4
+ export declare const MAX_PURL_LENGTH = 60;
5
+ export declare const MAX_TABLE_COLUMN_WIDTH = 30;
@@ -3,11 +3,13 @@ export const STATUS_COLORS = {
3
3
  EOL: 'red',
4
4
  UNKNOWN: 'default',
5
5
  OK: 'green',
6
- LTS: 'yellow',
6
+ SUPPORTED: 'yellow',
7
7
  };
8
8
  export const INDICATORS = {
9
9
  EOL: ux.colorize(STATUS_COLORS.EOL, '✗'),
10
10
  UNKNOWN: ux.colorize(STATUS_COLORS.UNKNOWN, '•'),
11
11
  OK: ux.colorize(STATUS_COLORS.OK, '✔'),
12
- LTS: ux.colorize(STATUS_COLORS.LTS, '⚡'),
12
+ SUPPORTED: ux.colorize(STATUS_COLORS.SUPPORTED, '⚡'),
13
13
  };
14
+ export const MAX_PURL_LENGTH = 60;
15
+ export const MAX_TABLE_COLUMN_WIDTH = 30;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herodevs/cli",
3
- "version": "1.4.0-beta.1",
3
+ "version": "1.5.0-beta.1",
4
4
  "author": "HeroDevs, Inc",
5
5
  "bin": {
6
6
  "hd": "./bin/run.js"
@@ -37,12 +37,15 @@
37
37
  "herodevs cli"
38
38
  ],
39
39
  "dependencies": {
40
- "@apollo/client": "^3.13.1",
41
- "@cyclonedx/cdxgen": "^11.2.3",
40
+ "@apollo/client": "^3.13.7",
41
+ "@cyclonedx/cdxgen": "^11.2.4",
42
42
  "@oclif/core": "^4",
43
43
  "@oclif/plugin-help": "^6",
44
44
  "@oclif/plugin-update": "^4",
45
- "graphql": "^16.8.1"
45
+ "@oclif/table": "^0.4.7",
46
+ "graphql": "^16.8.1",
47
+ "packageurl-js": "^2.0.1",
48
+ "update-notifier": "^7.3.1"
46
49
  },
47
50
  "devDependencies": {
48
51
  "@biomejs/biome": "^1.8.3",
@@ -50,13 +53,14 @@
50
53
  "@types/inquirer": "^9.0.7",
51
54
  "@types/node": "^22",
52
55
  "@types/sinon": "^17.0.4",
56
+ "@types/update-notifier": "^6.0.8",
53
57
  "globstar": "^1.0.0",
54
58
  "oclif": "^4",
55
- "shx": "^0.3.3",
56
- "sinon": "^19.0.2",
59
+ "shx": "^0.4.0",
60
+ "sinon": "^20.0.0",
57
61
  "ts-node": "^10",
58
62
  "tsx": "^4.19.3",
59
- "typescript": "^5.8.0"
63
+ "typescript": "^5.8.3"
60
64
  },
61
65
  "engines": {
62
66
  "node": ">=20.0.0"
@@ -78,6 +82,7 @@
78
82
  "@oclif/plugin-update"
79
83
  ],
80
84
  "hooks": {
85
+ "init": "./dist/hooks/npm-update-notifier",
81
86
  "prerun": "./dist/hooks/prerun.js"
82
87
  },
83
88
  "topicSeparator": " ",