Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@ npx @hailbytes/sbom-diff old.json new.json --format markdown

### Programmatic
```ts
import { diff } from '@hailbytes/sbom-diff';
import { readFile } from 'node:fs/promises';
import { parse, diff } from '@hailbytes/sbom-diff';

const report = await diff('old.cdx.json', 'new.cdx.json');
const oldSBOM = parse(await readFile('old.cdx.json', 'utf-8'));
const newSBOM = parse(await readFile('new.cdx.json', 'utf-8'));
const report = diff(oldSBOM, newSBOM);

console.log(report.added); // Component[] — newly added packages
console.log(report.removed); // Component[] — packages removed
console.log(report.upgraded); // { from: Component, to: Component }[]
console.log(report.newCVEs); // CVE[] — vulnerabilities in new packages
console.log(report.added); // Component[] — newly added packages
console.log(report.removed); // Component[] — packages removed
console.log(report.upgraded); // VersionChange[] — { component, from, to, isMajorBump }
console.log(report.newCVEs); // CVEEntry[] — vulnerabilities in new packages
```

---
Expand Down
28 changes: 28 additions & 0 deletions src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { resolveFormat } from '../cli.js';

describe('resolveFormat', () => {
it('defaults to text when --format is absent (regression for #5)', () => {
// Previously `args.indexOf('--format')` returned -1, so args[0] (the old
// file path) was used as the format and renderReport threw.
expect(resolveFormat(['old.json', 'new.json'])).toBe('text');
});

it('parses `--format json`', () => {
expect(resolveFormat(['old.json', 'new.json', '--format', 'json'])).toBe('json');
});

it('parses `--format=markdown`', () => {
expect(resolveFormat(['old.json', 'new.json', '--format=markdown'])).toBe('markdown');
});

it('defaults to text when --format has no value', () => {
expect(resolveFormat(['old.json', 'new.json', '--format'])).toBe('text');
});

it('throws a helpful error on an unsupported format', () => {
expect(() => resolveFormat(['old.json', 'new.json', '--format', 'xml'])).toThrow(
/Unsupported format: xml/,
);
});
});
50 changes: 43 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,46 @@
*/

import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import { parse } from './parser.js';
import { diff } from './diff.js';
import { renderReport } from './reporter.js';
import type { ReportFormat } from './types.js';

const VALID_FORMATS: readonly ReportFormat[] = ['text', 'json', 'markdown'];

/**
* Resolve the requested report format from CLI args.
*
* Supports both `--format=json` and `--format json` syntax. Defaults to
* 'text' when the flag is absent or has no value.
*
* @throws if a `--format` value is supplied that is not a supported format.
*/
export function resolveFormat(args: string[]): ReportFormat {
// `--format=value`
const inline = args.find(a => a.startsWith('--format='));
let value: string | undefined;
if (inline) {
value = inline.slice('--format='.length);
} else {
// `--format value`
const idx = args.indexOf('--format');
if (idx !== -1) {
const next = args[idx + 1];
if (next !== undefined && !next.startsWith('--')) value = next;
}
}

if (value === undefined || value === '') return 'text';
if (!VALID_FORMATS.includes(value as ReportFormat)) {
throw new Error(
`Unsupported format: ${value}. Valid formats: ${VALID_FORMATS.join(', ')}.`,
);
}
return value as ReportFormat;
}

async function main(): Promise<void> {
const args = process.argv.slice(2);

Expand All @@ -22,9 +57,7 @@ async function main(): Promise<void> {
}

const [oldPath, newPath] = positional;
const formatArg = args.find(a => a.startsWith('--format='))?.split('=')[1]
?? args[args.indexOf('--format') + 1];
const format: ReportFormat = (formatArg as ReportFormat) ?? 'text';
const format = resolveFormat(args);

const [oldRaw, newRaw] = await Promise.all([
readFile(oldPath, 'utf-8'),
Expand All @@ -38,7 +71,10 @@ async function main(): Promise<void> {
console.log(renderReport(report, format));
}

main().catch(err => {
console.error(err);
process.exit(1);
});
// Only run when invoked directly (not when imported by tests).
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(err => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});
}
Loading