@herodevs/cli 1.3.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.3.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
@@ -30,6 +30,7 @@ USAGE
30
30
  * [`hd report purls`](#hd-report-purls)
31
31
  * [`hd scan eol`](#hd-scan-eol)
32
32
  * [`hd scan sbom`](#hd-scan-sbom)
33
+ * [`hd update [CHANNEL]`](#hd-update-channel)
33
34
 
34
35
  ## `hd help [COMMAND]`
35
36
 
@@ -80,7 +81,7 @@ EXAMPLES
80
81
  $ hd report committers --csv
81
82
  ```
82
83
 
83
- _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.3.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)_
84
85
 
85
86
  ## `hd report purls`
86
87
 
@@ -114,7 +115,7 @@ EXAMPLES
114
115
  $ hd report purls --save --csv
115
116
  ```
116
117
 
117
- _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.3.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)_
118
119
 
119
120
  ## `hd scan eol`
120
121
 
@@ -122,15 +123,15 @@ Scan a given sbom for EOL data
122
123
 
123
124
  ```
124
125
  USAGE
125
- $ 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]
126
127
 
127
128
  FLAGS
128
- -a, --all Show all components (default is EOL and LTS only)
129
- -c, --getCustomerSupport Get Never-Ending Support for End-of-Life components
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
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
134
135
 
135
136
  GLOBAL FLAGS
136
137
  --json Format output as json.
@@ -148,7 +149,7 @@ EXAMPLES
148
149
  $ hd scan eol -a --dir=./my-project
149
150
  ```
150
151
 
151
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.3.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)_
152
153
 
153
154
  ## `hd scan sbom`
154
155
 
@@ -176,5 +177,43 @@ EXAMPLES
176
177
  $ hd scan sbom --file=path/to/sbom.json
177
178
  ```
178
179
 
179
- _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.3.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
+
182
+ ## `hd update [CHANNEL]`
183
+
184
+ update the hd CLI
185
+
186
+ ```
187
+ USAGE
188
+ $ hd update [CHANNEL] [--force | | [-a | -v <value> | -i]] [-b ]
189
+
190
+ FLAGS
191
+ -a, --available See available versions.
192
+ -b, --verbose Show more details about the available versions.
193
+ -i, --interactive Interactively select version to install. This is ignored if a channel is provided.
194
+ -v, --version=<value> Install a specific version.
195
+ --force Force a re-download of the requested version.
196
+
197
+ DESCRIPTION
198
+ update the hd CLI
199
+
200
+ EXAMPLES
201
+ Update to the stable channel:
202
+
203
+ $ hd update stable
204
+
205
+ Update to a specific version:
206
+
207
+ $ hd update --version 1.0.0
208
+
209
+ Interactively select version:
210
+
211
+ $ hd update --interactive
212
+
213
+ See available versions:
214
+
215
+ $ hd update --available
216
+ ```
217
+
218
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.37/src/commands/update.ts)_
180
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.3.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,11 +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
- "graphql": "^16.8.1"
44
+ "@oclif/plugin-update": "^4",
45
+ "@oclif/table": "^0.4.7",
46
+ "graphql": "^16.8.1",
47
+ "packageurl-js": "^2.0.1",
48
+ "update-notifier": "^7.3.1"
45
49
  },
46
50
  "devDependencies": {
47
51
  "@biomejs/biome": "^1.8.3",
@@ -49,13 +53,14 @@
49
53
  "@types/inquirer": "^9.0.7",
50
54
  "@types/node": "^22",
51
55
  "@types/sinon": "^17.0.4",
56
+ "@types/update-notifier": "^6.0.8",
52
57
  "globstar": "^1.0.0",
53
58
  "oclif": "^4",
54
- "shx": "^0.3.3",
55
- "sinon": "^19.0.2",
59
+ "shx": "^0.4.0",
60
+ "sinon": "^20.0.0",
56
61
  "ts-node": "^10",
57
62
  "tsx": "^4.19.3",
58
- "typescript": "^5.8.0"
63
+ "typescript": "^5.8.3"
59
64
  },
60
65
  "engines": {
61
66
  "node": ">=20.0.0"
@@ -73,12 +78,27 @@
73
78
  "commands": "./dist/commands",
74
79
  "plugins": [
75
80
  "@oclif/plugin-help",
76
- "@oclif/plugin-plugins"
81
+ "@oclif/plugin-plugins",
82
+ "@oclif/plugin-update"
77
83
  ],
78
84
  "hooks": {
85
+ "init": "./dist/hooks/npm-update-notifier",
79
86
  "prerun": "./dist/hooks/prerun.js"
80
87
  },
81
- "topicSeparator": " "
88
+ "topicSeparator": " ",
89
+ "macos": {
90
+ "identifier": "com.herodevs.cli"
91
+ },
92
+ "win": {
93
+ "icon": "assets/icon.ico"
94
+ },
95
+ "update": {
96
+ "s3": {
97
+ "bucket": "end-of-life-dataset-cli-releases",
98
+ "host": "https://end-of-life-dataset-cli-releases.s3.amazonaws.com",
99
+ "acl": "bucket-owner-full-control"
100
+ }
101
+ }
82
102
  },
83
103
  "types": "dist/index.d.ts"
84
104
  }