@herodevs/cli 1.0.0-beta.2 → 1.1.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.0.0-beta.2 linux-x64 node-v22.14.0
19
+ @herodevs/cli/1.1.0-beta.1 linux-x64 node-v22.14.0
20
20
  $ hd --help [COMMAND]
21
21
  USAGE
22
22
  $ hd COMMAND
@@ -357,13 +357,12 @@ Generate report of committers to a git repository
357
357
 
358
358
  ```
359
359
  USAGE
360
- $ hd report committers [--json] [-m <value>] [-o text|json|csv] [-s]
360
+ $ hd report committers [--json] [-m <value>] [-c] [-s]
361
361
 
362
362
  FLAGS
363
- -m, --months=<value> [default: 12] The number of months of git history to review
364
- -o, --output=<option> [default: text] Output format: text, json, or csv
365
- <options: text|json|csv>
366
- -s, --save Save the committers report as nes.committers.<output>
363
+ -c, --csv Output in CSV format
364
+ -m, --months=<value> [default: 12] The number of months of git history to review
365
+ -s, --save Save the committers report as nes.committers.<output>
367
366
 
368
367
  GLOBAL FLAGS
369
368
  --json Format output as json.
@@ -374,14 +373,14 @@ DESCRIPTION
374
373
  EXAMPLES
375
374
  $ hd report committers
376
375
 
377
- $ hd report committers -o csv -s
376
+ $ hd report committers --csv -s
378
377
 
379
- $ hd report committers --output=json
378
+ $ hd report committers --json
380
379
 
381
- $ hd report committers --output=csv
380
+ $ hd report committers --csv
382
381
  ```
383
382
 
384
- _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.0.0-beta.2/src/commands/report/committers.ts)_
383
+ _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.1.0-beta.1/src/commands/report/committers.ts)_
385
384
 
386
385
  ## `hd report purls`
387
386
 
@@ -389,14 +388,13 @@ Generate a list of purls from a sbom
389
388
 
390
389
  ```
391
390
  USAGE
392
- $ hd report purls [--json] [-f <value>] [-d <value>] [-s] [-o json|csv]
391
+ $ hd report purls [--json] [-f <value>] [-d <value>] [-s] [-c]
393
392
 
394
393
  FLAGS
395
- -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
396
- -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
397
- -o, --output=<option> [default: json] The format of the saved file (when using --save)
398
- <options: json|csv>
399
- -s, --save Save the list of purls as nes.purls.<output>
394
+ -c, --csv Save output in CSV format (only applies when using --save)
395
+ -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
396
+ -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
397
+ -s, --save Save the list of purls as nes.purls.<output>
400
398
 
401
399
  GLOBAL FLAGS
402
400
  --json Format output as json.
@@ -405,16 +403,18 @@ DESCRIPTION
405
403
  Generate a list of purls from a sbom
406
404
 
407
405
  EXAMPLES
406
+ $ hd report purls --json -s
407
+
408
408
  $ hd report purls --dir=./my-project
409
409
 
410
410
  $ hd report purls --file=path/to/sbom.json
411
411
 
412
412
  $ hd report purls --dir=./my-project --save
413
413
 
414
- $ hd report purls --save --output=csv
414
+ $ hd report purls --save --csv
415
415
  ```
416
416
 
417
- _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.0.0-beta.2/src/commands/report/purls.ts)_
417
+ _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.1.0-beta.1/src/commands/report/purls.ts)_
418
418
 
419
419
  ## `hd scan eol`
420
420
 
@@ -441,7 +441,7 @@ EXAMPLES
441
441
  $ hd scan eol --file=path/to/sbom.json
442
442
  ```
443
443
 
444
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.0.0-beta.2/src/commands/scan/eol.ts)_
444
+ _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.1.0-beta.1/src/commands/scan/eol.ts)_
445
445
 
446
446
  ## `hd scan sbom`
447
447
 
@@ -449,9 +449,10 @@ Scan a SBOM for purls
449
449
 
450
450
  ```
451
451
  USAGE
452
- $ hd scan sbom [--json] [-f <value>] [-d <value>] [-s]
452
+ $ hd scan sbom [--json] [-f <value>] [-d <value>] [-s] [-b]
453
453
 
454
454
  FLAGS
455
+ -b, --background Run the scan in the background
455
456
  -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
456
457
  -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
457
458
  -s, --save Save the generated SBOM as nes.sbom.json in the scanned directory
@@ -468,5 +469,5 @@ EXAMPLES
468
469
  $ hd scan sbom --file=path/to/sbom.json
469
470
  ```
470
471
 
471
- _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.0.0-beta.2/src/commands/scan/sbom.ts)_
472
+ _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.1.0-beta.1/src/commands/scan/sbom.ts)_
472
473
  <!-- commandsstop -->
package/bin/dev.js CHANGED
@@ -5,7 +5,6 @@ process.env.GRAPHQL_HOST = 'http://localhost:3000';
5
5
  async function run() {
6
6
  const oclif = await import('@oclif/core');
7
7
  await oclif.execute({ development: true, dir: import.meta.dirname });
8
- console.log('\n\n\n=> OCLIF: Command complete.');
9
8
  }
10
9
 
11
10
  run();
@@ -1,14 +1,15 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { type ReportData } from '../../service/committers.svc.ts';
2
3
  export default class Committers extends Command {
3
4
  static description: string;
4
5
  static enableJsonFlag: boolean;
5
6
  static examples: string[];
6
7
  static flags: {
7
8
  months: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
8
- output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  };
11
- run(): Promise<void>;
12
+ run(): Promise<ReportData | string>;
12
13
  /**
13
14
  * Generates structured report data
14
15
  * @param entries - parsed git log output for commits
@@ -2,15 +2,15 @@ import { spawnSync } from 'node:child_process';
2
2
  import { Command, Flags } from '@oclif/core';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { calculateOverallStats, formatOutputBasedOnFlag, groupCommitsByMonth, parseGitLogOutput, } from "../../service/committers.svc.js";
5
+ import { calculateOverallStats, formatAsCsv, formatAsText, groupCommitsByMonth, parseGitLogOutput, } from "../../service/committers.svc.js";
6
6
  export default class Committers extends Command {
7
7
  static description = 'Generate report of committers to a git repository';
8
8
  static enableJsonFlag = true;
9
9
  static examples = [
10
10
  '<%= config.bin %> <%= command.id %>',
11
- '<%= config.bin %> <%= command.id %> -o csv -s',
12
- '<%= config.bin %> <%= command.id %> --output=json',
13
- '<%= config.bin %> <%= command.id %> --output=csv',
11
+ '<%= config.bin %> <%= command.id %> --csv -s',
12
+ '<%= config.bin %> <%= command.id %> --json',
13
+ '<%= config.bin %> <%= command.id %> --csv',
14
14
  ];
15
15
  static flags = {
16
16
  months: Flags.integer({
@@ -18,11 +18,10 @@ export default class Committers extends Command {
18
18
  description: 'The number of months of git history to review',
19
19
  default: 12,
20
20
  }),
21
- output: Flags.string({
22
- char: 'o',
23
- description: 'Output format: text, json, or csv',
24
- options: ['text', 'json', 'csv'],
25
- default: 'text',
21
+ csv: Flags.boolean({
22
+ char: 'c',
23
+ description: 'Output in CSV format',
24
+ default: false,
26
25
  }),
27
26
  save: Flags.boolean({
28
27
  char: 's',
@@ -32,24 +31,50 @@ export default class Committers extends Command {
32
31
  };
33
32
  async run() {
34
33
  const { flags } = await this.parse(Committers);
35
- const { months, output, save } = flags;
34
+ const { months, csv, save } = flags;
35
+ const isJson = this.jsonEnabled();
36
36
  const sinceDate = `${months} months ago`;
37
+ this.log('Starting committers report with flags: %O', flags);
37
38
  try {
38
39
  // Generate structured report data
39
40
  const entries = this.fetchGitCommitData(sinceDate);
41
+ this.log('Fetched %d commit entries', entries.length);
40
42
  const reportData = this.generateReportData(entries);
41
- const formattedOutput = formatOutputBasedOnFlag(output, reportData);
42
- // Output to file or stdout
43
+ // Handle different output scenarios
44
+ if (isJson) {
45
+ // JSON mode
46
+ if (save) {
47
+ fs.writeFileSync(path.resolve('nes.committers.json'), JSON.stringify(reportData, null, 2));
48
+ this.log('Report written to json');
49
+ }
50
+ return reportData;
51
+ }
52
+ if (csv) {
53
+ // CSV mode
54
+ const csvOutput = formatAsCsv(reportData);
55
+ if (save) {
56
+ fs.writeFileSync(path.resolve('nes.committers.csv'), csvOutput);
57
+ this.log('Report written to csv');
58
+ }
59
+ else {
60
+ this.log(csvOutput);
61
+ }
62
+ return csvOutput;
63
+ }
64
+ // Text mode
65
+ const textOutput = formatAsText(reportData);
43
66
  if (save) {
44
- fs.writeFileSync(path.resolve(`nes.committers.${output}`), formattedOutput);
45
- this.log(`Report written to ${output}`);
67
+ fs.writeFileSync(path.resolve('nes.committers.txt'), textOutput);
68
+ this.log('Report written to txt');
46
69
  }
47
70
  else {
48
- this.log(formattedOutput);
71
+ this.log(textOutput);
49
72
  }
73
+ return textOutput;
50
74
  }
51
75
  catch (error) {
52
76
  this.error(`Failed to generate report: ${error.message}`);
77
+ throw error;
53
78
  }
54
79
  }
55
80
  /**
@@ -7,7 +7,9 @@ export default class ReportPurls extends Command {
7
7
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
9
  save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
- output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  };
12
- run(): Promise<string[]>;
12
+ run(): Promise<{
13
+ purls: string[];
14
+ }>;
13
15
  }
@@ -7,10 +7,11 @@ export default class ReportPurls extends Command {
7
7
  static description = 'Generate a list of purls from a sbom';
8
8
  static enableJsonFlag = true;
9
9
  static examples = [
10
+ '<%= config.bin %> <%= command.id %> --json -s',
10
11
  '<%= config.bin %> <%= command.id %> --dir=./my-project',
11
12
  '<%= config.bin %> <%= command.id %> --file=path/to/sbom.json',
12
13
  '<%= config.bin %> <%= command.id %> --dir=./my-project --save',
13
- '<%= config.bin %> <%= command.id %> --save --output=csv',
14
+ '<%= config.bin %> <%= command.id %> --save --csv',
14
15
  ];
15
16
  static flags = {
16
17
  file: Flags.string({
@@ -26,41 +27,45 @@ export default class ReportPurls extends Command {
26
27
  default: false,
27
28
  description: 'Save the list of purls as nes.purls.<output>',
28
29
  }),
29
- output: Flags.string({
30
- char: 'o',
31
- options: ['json', 'csv'],
32
- default: 'json',
33
- description: 'The format of the saved file (when using --save)',
30
+ csv: Flags.boolean({
31
+ char: 'c',
32
+ default: false,
33
+ description: 'Save output in CSV format (only applies when using --save)',
34
34
  }),
35
35
  };
36
36
  async run() {
37
37
  const { flags } = await this.parse(ReportPurls);
38
- const { dir: _dirFlag, file: _fileFlag, save, output } = flags;
39
- // Load the SBOM: Only pass the file, dir, and save flags to SbomScan
38
+ const { dir: _dirFlag, file: _fileFlag, save, csv } = flags;
40
39
  const sbomArgs = SbomScan.getSbomArgs(flags);
41
40
  const sbomCommand = new SbomScan(sbomArgs, this.config);
42
41
  const sbom = await sbomCommand.run();
43
- // Extract purls from SBOM
42
+ if (!sbom) {
43
+ throw new Error('SBOM not generated');
44
+ }
44
45
  const purls = await extractPurls(sbom);
46
+ this.log('Extracted %d purls from SBOM', purls.length);
45
47
  ux.action.stop('Scan completed');
46
48
  // Print the purls
47
- this.log('Found purls:');
48
49
  for (const purl of purls) {
49
50
  this.log(purl);
50
51
  }
51
52
  // Save if requested
52
53
  if (save) {
53
54
  try {
54
- const outputPath = path.join(_dirFlag || process.cwd(), `nes.purls.${output}`);
55
- const purlOutput = getPurlOutput(purls, output);
55
+ const outputFile = csv && !this.jsonEnabled() ? 'csv' : 'json';
56
+ const outputPath = path.join(_dirFlag || process.cwd(), `nes.purls.${outputFile}`);
57
+ const purlOutput = getPurlOutput(purls, outputFile);
56
58
  fs.writeFileSync(outputPath, purlOutput);
57
- this.log(`\nPurls saved to ${outputPath}`);
59
+ this.log('Purls saved to %s', outputPath);
58
60
  }
59
61
  catch (error) {
60
62
  const errorMessage = error && typeof error === 'object' && 'message' in error ? error.message : 'Unknown error';
61
63
  this.warn(`Failed to save purls: ${errorMessage}`);
62
64
  }
63
65
  }
64
- return purls;
66
+ // Return wrapped object with metadata
67
+ return {
68
+ purls,
69
+ };
65
70
  }
66
71
  }
@@ -32,6 +32,9 @@ export default class ScanEol extends Command {
32
32
  const sbomArgs = SbomScan.getSbomArgs(flags);
33
33
  const sbomCommand = new SbomScan(sbomArgs, this.config);
34
34
  const sbom = await sbomCommand.run();
35
+ if (!sbom) {
36
+ throw new Error('SBOM not generated');
37
+ }
35
38
  // Scan the SBOM for EOL information
36
39
  const { purls, scan } = await scanForEol(sbom);
37
40
  ux.action.stop('Scan completed');
@@ -8,11 +8,13 @@ export default class ScanSbom extends Command {
8
8
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
9
  dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ background: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
12
  };
12
13
  static getSbomArgs(flags: Record<string, string>): string[];
13
14
  getScanOptions(): {};
14
- run(): Promise<Sbom>;
15
+ run(): Promise<Sbom | undefined>;
15
16
  private _getSbomFromScan;
17
+ private _getSbomInBackground;
16
18
  private _getSbomFromFile;
17
19
  private _saveSbom;
18
20
  }
@@ -1,6 +1,7 @@
1
- import path from 'node:path';
2
- import { Command, Flags, ux } from '@oclif/core';
1
+ import { spawn } from 'node:child_process';
3
2
  import fs from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { Command, Flags, ux } from '@oclif/core';
4
5
  import { createSbom, validateIsCycloneDxSbom } from "../../service/eol/eol.svc.js";
5
6
  export default class ScanSbom extends Command {
6
7
  static description = 'Scan a SBOM for purls';
@@ -23,9 +24,14 @@ export default class ScanSbom extends Command {
23
24
  default: false,
24
25
  description: 'Save the generated SBOM as nes.sbom.json in the scanned directory',
25
26
  }),
27
+ background: Flags.boolean({
28
+ char: 'b',
29
+ default: false,
30
+ description: 'Run the scan in the background',
31
+ }),
26
32
  };
27
33
  static getSbomArgs(flags) {
28
- const { dir, file, save } = flags ?? {};
34
+ const { dir, file, save, json, background } = flags ?? {};
29
35
  const sbomArgs = [];
30
36
  if (file)
31
37
  sbomArgs.push('--file', file);
@@ -33,6 +39,10 @@ export default class ScanSbom extends Command {
33
39
  sbomArgs.push('--dir', dir);
34
40
  if (save)
35
41
  sbomArgs.push('--save');
42
+ if (json)
43
+ sbomArgs.push('--json');
44
+ if (background)
45
+ sbomArgs.push('--background');
36
46
  return sbomArgs;
37
47
  }
38
48
  getScanOptions() {
@@ -41,26 +51,31 @@ export default class ScanSbom extends Command {
41
51
  }
42
52
  async run() {
43
53
  const { flags } = await this.parse(ScanSbom);
44
- const { dir: _dirFlag, save, file: _fileFlag } = flags;
54
+ const { dir, save, file, background } = flags;
45
55
  // Validate that exactly one of --file or --dir is provided
46
- if (_fileFlag && _dirFlag) {
56
+ if (file && dir) {
47
57
  throw new Error('Cannot specify both --file and --dir flags. Please use one or the other.');
48
58
  }
49
59
  let sbom;
50
- if (_fileFlag) {
51
- sbom = this._getSbomFromFile(_fileFlag);
60
+ const path = dir || process.cwd();
61
+ if (file) {
62
+ sbom = this._getSbomFromFile(file);
63
+ }
64
+ else if (background) {
65
+ this._getSbomInBackground(path);
66
+ this.log(`The scan is running in the background. The file will be saved at ${path}/nes.sbom.json`);
67
+ return;
52
68
  }
53
69
  else {
54
- const _dir = _dirFlag || process.cwd();
55
- sbom = await this._getSbomFromScan(_dir);
70
+ sbom = await this._getSbomFromScan(path);
56
71
  if (save) {
57
- this._saveSbom(_dir, sbom);
72
+ this._saveSbom(path, sbom);
58
73
  }
59
74
  }
60
75
  return sbom;
61
76
  }
62
77
  async _getSbomFromScan(_dirFlag) {
63
- const dir = path.resolve(_dirFlag);
78
+ const dir = resolve(_dirFlag);
64
79
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
65
80
  throw new Error(`Directory not found or not a directory: ${dir}`);
66
81
  }
@@ -72,8 +87,23 @@ export default class ScanSbom extends Command {
72
87
  }
73
88
  return sbom;
74
89
  }
90
+ _getSbomInBackground(path) {
91
+ const opts = this.getScanOptions();
92
+ const args = [
93
+ JSON.stringify({
94
+ opts,
95
+ path,
96
+ }),
97
+ ];
98
+ const workerProcess = spawn('node', [join(import.meta.dirname, '../../service/eol/sbom.worker.js'), ...args], {
99
+ stdio: 'ignore',
100
+ detached: true,
101
+ env: { ...process.env },
102
+ });
103
+ workerProcess.unref();
104
+ }
75
105
  _getSbomFromFile(_fileFlag) {
76
- const file = path.resolve(_fileFlag);
106
+ const file = resolve(_fileFlag);
77
107
  if (!fs.existsSync(file)) {
78
108
  throw new Error(`SBOM file not found: ${file}`);
79
109
  }
@@ -94,9 +124,11 @@ export default class ScanSbom extends Command {
94
124
  }
95
125
  _saveSbom(dir, sbom) {
96
126
  try {
97
- const outputPath = path.join(dir, 'nes.sbom.json');
127
+ const outputPath = join(dir, 'nes.sbom.json');
98
128
  fs.writeFileSync(outputPath, JSON.stringify(sbom, null, 2));
99
- this.log(`SBOM saved to ${outputPath}`);
129
+ if (!this.jsonEnabled()) {
130
+ this.log(`SBOM saved to ${outputPath}`);
131
+ }
100
132
  }
101
133
  catch (error) {
102
134
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@@ -0,0 +1,8 @@
1
+ import debug from 'debug';
2
+ const hook = async (opts) => {
3
+ // If JSON flag is enabled, silence debug logging
4
+ if (opts.Command.prototype.jsonEnabled()) {
5
+ debug.disable();
6
+ }
7
+ };
8
+ export default hook;
@@ -9,6 +9,58 @@ export interface Sbom {
9
9
  components: SbomEntry[];
10
10
  dependencies: SbomEntry[];
11
11
  }
12
+ export declare const SBOM_DEFAULT__OPTIONS: {
13
+ $0: string;
14
+ _: never[];
15
+ 'auto-compositions': boolean;
16
+ autoCompositions: boolean;
17
+ 'data-flow-slices-file': string;
18
+ dataFlowSlicesFile: string;
19
+ deep: boolean;
20
+ 'deps-slices-file': string;
21
+ depsSlicesFile: string;
22
+ evidence: boolean;
23
+ 'export-proto': boolean;
24
+ exportProto: boolean;
25
+ 'fail-on-error': boolean;
26
+ failOnError: boolean;
27
+ false: boolean;
28
+ 'include-crypto': boolean;
29
+ 'include-formulation': boolean;
30
+ includeCrypto: boolean;
31
+ includeFormulation: boolean;
32
+ 'install-deps': boolean;
33
+ installDeps: boolean;
34
+ 'min-confidence': number;
35
+ minConfidence: number;
36
+ multiProject: boolean;
37
+ 'no-banner': boolean;
38
+ noBabel: boolean;
39
+ noBanner: boolean;
40
+ o: string;
41
+ output: string;
42
+ outputFormat: string;
43
+ profile: string;
44
+ project: undefined;
45
+ 'project-version': string;
46
+ projectVersion: string;
47
+ 'proto-bin-file': string;
48
+ protoBinFile: string;
49
+ r: boolean;
50
+ 'reachables-slices-file': string;
51
+ reachablesSlicesFile: string;
52
+ recurse: boolean;
53
+ requiredOnly: boolean;
54
+ 'semantics-slices-file': string;
55
+ semanticsSlicesFile: string;
56
+ 'skip-dt-tls-check': boolean;
57
+ skipDtTlsCheck: boolean;
58
+ 'spec-version': number;
59
+ specVersion: number;
60
+ 'usages-slices-file': string;
61
+ usagesSlicesFile: string;
62
+ validate: boolean;
63
+ };
12
64
  /**
13
65
  * Lazy loads cdxgen (for ESM purposes), scans
14
66
  * `directory`, and returns the `bomJson` property.
@@ -1,71 +1,66 @@
1
- import { log } from "../../service/log.svc.js";
1
+ import { debugLogger } from "../../service/log.svc.js";
2
+ export const SBOM_DEFAULT__OPTIONS = {
3
+ $0: 'cdxgen',
4
+ _: [],
5
+ 'auto-compositions': true,
6
+ autoCompositions: true,
7
+ 'data-flow-slices-file': 'data-flow.slices.json',
8
+ dataFlowSlicesFile: 'data-flow.slices.json',
9
+ deep: false, // TODO: you def want to check this out
10
+ 'deps-slices-file': 'deps.slices.json',
11
+ depsSlicesFile: 'deps.slices.json',
12
+ evidence: false,
13
+ 'export-proto': false,
14
+ exportProto: false,
15
+ // DON'T FAIL ON ERROR; you won't get hlepful logs
16
+ 'fail-on-error': false,
17
+ failOnError: false,
18
+ false: true,
19
+ 'include-crypto': false,
20
+ 'include-formulation': false,
21
+ includeCrypto: false,
22
+ includeFormulation: false,
23
+ 'install-deps': true,
24
+ installDeps: true,
25
+ 'min-confidence': 0,
26
+ minConfidence: 0,
27
+ multiProject: true,
28
+ 'no-banner': false,
29
+ noBabel: false,
30
+ noBanner: false,
31
+ o: 'bom.json',
32
+ output: 'bom.json',
33
+ outputFormat: 'json', // or "xml"
34
+ // author: ['OWASP Foundation'],
35
+ profile: 'generic',
36
+ project: undefined,
37
+ 'project-version': '',
38
+ projectVersion: '',
39
+ 'proto-bin-file': 'bom.cdx',
40
+ protoBinFile: 'bom.cdx',
41
+ r: false,
42
+ 'reachables-slices-file': 'reachables.slices.json',
43
+ reachablesSlicesFile: 'reachables.slices.json',
44
+ recurse: false,
45
+ requiredOnly: false,
46
+ 'semantics-slices-file': 'semantics.slices.json',
47
+ semanticsSlicesFile: 'semantics.slices.json',
48
+ 'skip-dt-tls-check': true,
49
+ skipDtTlsCheck: true,
50
+ 'spec-version': 1.6,
51
+ specVersion: 1.6,
52
+ 'usages-slices-file': 'usages.slices.json',
53
+ usagesSlicesFile: 'usages.slices.json',
54
+ validate: true,
55
+ };
2
56
  /**
3
57
  * Lazy loads cdxgen (for ESM purposes), scans
4
58
  * `directory`, and returns the `bomJson` property.
5
59
  */
6
60
  export async function createBomFromDir(directory, opts = {}) {
7
- const options = {
8
- $0: 'cdxgen',
9
- _: [],
10
- 'auto-compositions': true,
11
- autoCompositions: true,
12
- 'data-flow-slices-file': 'data-flow.slices.json',
13
- dataFlowSlicesFile: 'data-flow.slices.json',
14
- deep: false, // TODO: you def want to check this out
15
- 'deps-slices-file': 'deps.slices.json',
16
- depsSlicesFile: 'deps.slices.json',
17
- evidence: false,
18
- 'export-proto': false,
19
- exportProto: false,
20
- // DON'T FAIL ON ERROR; you won't get hlepful logs
21
- 'fail-on-error': false,
22
- failOnError: false,
23
- false: true,
24
- 'include-crypto': false,
25
- 'include-formulation': false,
26
- includeCrypto: false,
27
- includeFormulation: false,
28
- // 'server-host': '127.0.0.1',
29
- // serverHost: '127.0.0.1',
30
- // 'server-port': '9090',
31
- // serverPort: '9090',
32
- 'install-deps': true,
33
- installDeps: true,
34
- 'min-confidence': 0,
35
- minConfidence: 0,
36
- multiProject: true,
37
- 'no-banner': false,
38
- noBabel: false,
39
- noBanner: false,
40
- o: 'bom.json',
41
- output: 'bom.json',
42
- outputFormat: 'json', // or "xml"
43
- // author: ['OWASP Foundation'],
44
- profile: 'generic',
45
- project: undefined,
46
- 'project-version': '',
47
- projectVersion: '',
48
- 'proto-bin-file': 'bom.cdx',
49
- protoBinFile: 'bom.cdx',
50
- r: false,
51
- 'reachables-slices-file': 'reachables.slices.json',
52
- reachablesSlicesFile: 'reachables.slices.json',
53
- recurse: false,
54
- requiredOnly: false,
55
- 'semantics-slices-file': 'semantics.slices.json',
56
- semanticsSlicesFile: 'semantics.slices.json',
57
- 'skip-dt-tls-check': true,
58
- skipDtTlsCheck: true,
59
- 'spec-version': 1.6,
60
- specVersion: 1.6,
61
- 'usages-slices-file': 'usages.slices.json',
62
- usagesSlicesFile: 'usages.slices.json',
63
- validate: true,
64
- ...opts,
65
- };
66
61
  const { createBom } = await getCdxGen();
67
- const sbom = await createBom?.(directory, options);
68
- log.info('Successfully generated SBOM');
62
+ const sbom = await createBom?.(directory, { ...SBOM_DEFAULT__OPTIONS, ...opts });
63
+ debugLogger('Successfully generated SBOM');
69
64
  return sbom?.bomJson;
70
65
  }
71
66
  // use a value holder, for easier mocking
@@ -1,5 +1,5 @@
1
1
  import { NesApolloClient } from "../../api/nes/nes.client.js";
2
- import { log } from "../../service/log.svc.js";
2
+ import { debugLogger } from "../../service/log.svc.js";
3
3
  import { getDaysEolFromEolAt, getStatusFromComponent } from "../line.svc.js";
4
4
  import { extractPurls } from "../purls.svc.js";
5
5
  import { createBomFromDir } from "./cdx.svc.js";
@@ -7,7 +7,7 @@ export async function createSbom(directory, opts = {}) {
7
7
  const sbom = await createBomFromDir(directory, opts.cdxgen || {});
8
8
  if (!sbom)
9
9
  throw new Error('SBOM not generated');
10
- log.info('SBOM generated');
10
+ debugLogger('SBOM generated');
11
11
  return sbom;
12
12
  }
13
13
  export function validateIsCycloneDxSbom(sbom) {
@@ -60,7 +60,7 @@ export async function prepareRows(purls, scan) {
60
60
  if (!details) {
61
61
  // In this case, the purl string is in the generated sbom, but the NES/XEOL api has no data
62
62
  // TODO: add UNKNOWN Component Status, create new line, and create flag to show/hide unknown results
63
- log.debug(`Unknown status: ${purl}.`);
63
+ debugLogger(`Unknown status: ${purl}.`);
64
64
  continue;
65
65
  }
66
66
  const { info } = details;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createBom } from '@cyclonedx/cdxgen';
4
+ import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
5
+ process.on('uncaughtException', (err) => {
6
+ console.error('Uncaught exception:', err.message);
7
+ process.exit(1);
8
+ });
9
+ process.on('unhandledRejection', (reason) => {
10
+ console.error('Unhandled rejection:', reason);
11
+ process.exit(1);
12
+ });
13
+ try {
14
+ console.log('Sbom worker started');
15
+ const options = JSON.parse(process.argv[2]);
16
+ const { path, opts } = options;
17
+ const { bomJson } = await createBom(path, { ...SBOM_DEFAULT__OPTIONS, ...opts });
18
+ const outputPath = join(path, 'nes.sbom.json');
19
+ writeFileSync(outputPath, JSON.stringify(bomJson, null, 2));
20
+ process.exit(0);
21
+ }
22
+ catch (error) {
23
+ console.error('Error creating SBOM', error.message);
24
+ process.exit(1);
25
+ }
@@ -1,10 +1,7 @@
1
+ import debug from 'debug';
1
2
  /**
2
- * A simple logging construct when you
3
- * don't have the command instance handy
3
+ * A simple debug logger for services.
4
+ * Services should only use debug logging for development/troubleshooting.
5
+ * All user-facing output should be handled by commands.
4
6
  */
5
- export declare const log: {
6
- info: (_message?: unknown, ...args: unknown[]) => void;
7
- warn: (_message?: unknown, ...args: unknown[]) => void;
8
- debug: (_message?: unknown, ...args: unknown[]) => void;
9
- };
10
- export declare const initOclifLog: (info: (message?: unknown, ...args: unknown[]) => void, warn: (message?: unknown, ...args: unknown[]) => void, debug: (message?: unknown, ...args: unknown[]) => void) => void;
7
+ export declare const debugLogger: debug.Debugger;
@@ -1,20 +1,7 @@
1
+ import debug from 'debug';
1
2
  /**
2
- * A simple logging construct when you
3
- * don't have the command instance handy
3
+ * A simple debug logger for services.
4
+ * Services should only use debug logging for development/troubleshooting.
5
+ * All user-facing output should be handled by commands.
4
6
  */
5
- export const log = {
6
- info: (_message, ...args) => {
7
- console.log('[default_log]', ...args);
8
- },
9
- warn: (_message, ...args) => {
10
- console.warn('[default_warn]', ...args);
11
- },
12
- debug: (_message, ...args) => {
13
- console.debug('[default_debug]', ...args);
14
- },
15
- };
16
- export const initOclifLog = (info, warn, debug) => {
17
- log.info = info;
18
- log.warn = warn;
19
- log.debug = debug;
20
- };
7
+ export const debugLogger = debug('oclif:herodevs-debug');
@@ -1,5 +1,5 @@
1
1
  import { M_SCAN } from "../../api/queries/nes/sbom.js";
2
- import { log } from "../log.svc.js";
2
+ 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) {
@@ -16,8 +16,8 @@ export const SbomScanner = (client) => async (purls) => {
16
16
  const res = await client.mutate(M_SCAN.gql, { input });
17
17
  const scan = res.data?.insights?.scan?.eol;
18
18
  if (!scan?.success) {
19
- log.info('failed scan %o', scan || {});
20
- log.warn('scan failed');
19
+ debugLogger('failed scan %o', scan || {});
20
+ debugLogger('scan failed');
21
21
  throw new Error('Failed to provide scan: ');
22
22
  }
23
23
  const result = buildScanResult(scan);
@@ -17,7 +17,7 @@ export function getPurlOutput(purls, output) {
17
17
  case 'csv':
18
18
  return ['purl', ...purls].map(formatCsvValue).join('\n');
19
19
  default:
20
- return JSON.stringify(purls, null, 2);
20
+ return JSON.stringify({ purls }, null, 2);
21
21
  }
22
22
  }
23
23
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herodevs/cli",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.1.0-beta.1",
4
4
  "author": "HeroDevs, Inc",
5
5
  "bin": {
6
6
  "hd": "./bin/run.js"
@@ -19,6 +19,7 @@
19
19
  "clean": "shx rm -rf dist && npm run clean:files && shx rm -rf node_modules",
20
20
  "clean:files": "shx rm -f nes.**.csv nes.**.json nes.**.text",
21
21
  "dev": "npm run build && ./bin/dev.js",
22
+ "dev:debug": "npm run build && DEBUG=* ./bin/dev.js",
22
23
  "format": "biome format --write",
23
24
  "lint": "biome lint --write",
24
25
  "postpack": "shx rm -f oclif.manifest.json",
@@ -52,6 +53,7 @@
52
53
  "oclif": "^4",
53
54
  "shx": "^0.3.3",
54
55
  "sinon": "^19.0.2",
56
+ "ts-node": "^10",
55
57
  "typescript": "^5.8.0"
56
58
  },
57
59
  "engines": {
@@ -70,15 +72,12 @@
70
72
  "commands": "./dist/commands",
71
73
  "plugins": [
72
74
  "@oclif/plugin-help",
73
- "@oclif/plugin-plugins",
74
- "@oclif/plugin-update"
75
+ "@oclif/plugin-plugins"
75
76
  ],
76
- "topicSeparator": " ",
77
- "update": {
78
- "node": {
79
- "version": ">=22.0.0"
80
- }
81
- }
77
+ "hooks": {
78
+ "prerun": "./dist/hooks/prerun.js"
79
+ },
80
+ "topicSeparator": " "
82
81
  },
83
82
  "types": "dist/index.d.ts"
84
83
  }
@@ -1,2 +0,0 @@
1
- import type { Hook } from '@oclif/core';
2
- export declare const initUpdate: Hook<'init'>;
@@ -1,5 +0,0 @@
1
- import updateConfig from '../../config/update.js';
2
- export const initUpdate = async ({ config }) => {
3
- // Apply update configuration
4
- Object.assign(config, updateConfig);
5
- };
@@ -1,8 +0,0 @@
1
- import { initOclifLog, log } from "../../service/log.svc.js";
2
- const hook = async (opts) => {
3
- initOclifLog(opts.context.log, opts.context.log, opts.context.debug);
4
- log.info = opts.context.log || log.info;
5
- log.warn = opts.context.log || log.warn;
6
- log.debug = opts.context.debug || log.debug;
7
- };
8
- export default hook;