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
39 changes: 39 additions & 0 deletions src/__tests__/args.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, it, expect } from 'vitest';
import { parseArgs, ArgError } from '../args.js';

describe('parseArgs', () => {
it('defaults to text format when --format is omitted', () => {
// Regression: previously `args.indexOf('--format')` returned -1 and the
// first positional arg was mis-read as the format, crashing the headline
// `sbom-diff old.json new.json` command.
const parsed = parseArgs(['old.json', 'new.json']);
expect(parsed.oldPath).toBe('old.json');
expect(parsed.newPath).toBe('new.json');
expect(parsed.format).toBe('text');
});

it('parses --format=value (inline form)', () => {
expect(parseArgs(['old.json', 'new.json', '--format=json']).format).toBe('json');
});

it('parses --format value (space-separated form)', () => {
expect(parseArgs(['old.json', 'new.json', '--format', 'markdown']).format).toBe('markdown');
});

it('accepts the flag before positional arguments', () => {
const parsed = parseArgs(['--format', 'json', 'old.json', 'new.json']);
expect(parsed.format).toBe('json');
expect(parsed.oldPath).toBe('old.json');
expect(parsed.newPath).toBe('new.json');
});

it('throws ArgError when fewer than two files are given', () => {
expect(() => parseArgs(['old.json'])).toThrow(ArgError);
expect(() => parseArgs([])).toThrow(ArgError);
});

it('throws ArgError on an unsupported format', () => {
expect(() => parseArgs(['old.json', 'new.json', '--format', 'xml'])).toThrow(ArgError);
expect(() => parseArgs(['old.json', 'new.json', '--format=xml'])).toThrow(/Invalid format/);
});
});
65 changes: 65 additions & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { ReportFormat } from './types.js';

/** Result of parsing the CLI arguments. */
export interface ParsedArgs {
oldPath: string;
newPath: string;
format: ReportFormat;
}

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

/** A user-facing argument error. The CLI turns this into a clean message + non-zero exit. */
export class ArgError extends Error {}

/**
* Parse the CLI argument vector (everything after `node cli.js`).
*
* Supports both `--format json` and `--format=json`, in any position. When
* `--format` is omitted the report defaults to `text`.
*
* @throws {ArgError} when positional args are missing or the format is invalid.
*/
export function parseArgs(argv: string[]): ParsedArgs {
const positional: string[] = [];
let format: ReportFormat | undefined;

for (let i = 0; i < argv.length; i++) {
const arg = argv[i];

if (arg.startsWith('--format=')) {
format = validateFormat(arg.slice('--format='.length));
} else if (arg === '--format') {
// Consume the next token as the value so it is not mistaken for a file.
format = validateFormat(argv[++i]);
} else if (arg.startsWith('--')) {
throw new ArgError(`Unknown option: ${arg}`);
} else {
positional.push(arg);
}
}

if (positional.length < 2) {
throw new ArgError(
'Usage: sbom-diff <old.json> <new.json> [--format text|json|markdown]',
);
}

const [oldPath, newPath] = positional;
return { oldPath, newPath, format: format ?? 'text' };
}

/** Validate a `--format` value, throwing a friendly error for anything unsupported. */
function validateFormat(value: string | undefined): ReportFormat {
if (value === undefined || value === '') {
throw new ArgError(
`Missing value for --format. Supported formats: ${SUPPORTED_FORMATS.join(', ')}.`,
);
}
if (!SUPPORTED_FORMATS.includes(value as ReportFormat)) {
throw new ArgError(
`Invalid format: "${value}". Supported formats: ${SUPPORTED_FORMATS.join(', ')}.`,
);
}
return value as ReportFormat;
}
24 changes: 9 additions & 15 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,10 @@ import { readFile } from 'node:fs/promises';
import { parse } from './parser.js';
import { diff } from './diff.js';
import { renderReport } from './reporter.js';
import type { ReportFormat } from './types.js';
import { parseArgs, ArgError } from './args.js';

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

const positional = args.filter(a => !a.startsWith('--'));
if (positional.length < 2) {
console.error('Usage: sbom-diff <old.json> <new.json> [--format text|json|markdown]');
process.exit(1);
}

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 { oldPath, newPath, format } = parseArgs(process.argv.slice(2));

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

main().catch(err => {
console.error(err);
main().catch((err) => {
// Argument errors are user-facing: show a clean message, not a stack trace.
if (err instanceof ArgError) {
console.error(err.message);
} else {
console.error(err);
}
process.exit(1);
});
Loading