Skip to content
16 changes: 16 additions & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,22 @@ export function parseArgs(argv: string[]): {
options.fix = true;
continue;
}
if (arg === "--create-pr") {
options.createPr = true;
continue;
}
if (arg === "--base") {
const val = argv[++i];
if (!val) throw new Error("--base requires a branch name");
options.prBase = val;
continue;
}
if (arg.startsWith("--base=")) {
const val = arg.slice("--base=".length);
if (!val) throw new Error("--base requires a branch name");
options.prBase = val;
continue;
}
if (arg === "--prod-only") {
options.prodOnly = true;
continue;
Expand Down
2 changes: 2 additions & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export function printHelp(): void {
" --cdx Write CycloneDX 1.4 SBOM to a timestamped .cdx.json file",
" --no-open Don't auto-open the report in the browser",
" --fix Apply validated direct dependency fixes and rescan",
" --create-pr After --fix, commit changes and open a GitHub pull request (requires gh)",
" --base <branch> Base branch for --create-pr (default: main)",
" --osv-url <url> Use a custom OSV-compatible advisory endpoint",
" --ca-cert <path> Path to a CA certificate file for corporate SSL proxies",
" --debug Write verbose runtime/network diagnostics to a timestamped log file",
Expand Down
12 changes: 12 additions & 0 deletions src/cli/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ export function validateOptions(options: ParsedOptions): void {
throw new Error("--fix cannot be used with --json");
}

if (options.createPr && !options.fix) {
throw new Error("--create-pr requires --fix");
}

if (options.createPr && options.json) {
throw new Error("--create-pr cannot be used with --json");
}

if (options.prBase && !options.createPr) {
throw new Error("--base can only be used with --create-pr");
}

if (options.report && options.json) {
throw new Error("--report cannot be used with --json");
}
Expand Down
37 changes: 34 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ import {
} from "./utils/fix-runner.js";
import { hasRootLockfile, findNestedLockfiles } from "./parsers/multi-package.js";
import { handleMultiFolderScan } from "./scan/multi-folder-scan.js";
import {
createPullRequestForFixes,
findingsMeetFailOnThreshold,
} from "./utils/create-pr.js";
let parsedArgs: ReturnType<typeof parseArgs> | null = null;
try {
parsedArgs = parseArgs(process.argv.slice(2));
Expand Down Expand Up @@ -181,7 +185,7 @@ if (parsedArgs) {
return;
}

let advisorySourceLine: string;
let advisorySourceLine = "";
let advisoryDbFreshnessLine: string | null = null;
let advisoryDbWarning: string | null = null;
try {
Expand Down Expand Up @@ -219,7 +223,9 @@ if (parsedArgs) {
if (options.offline || options.offlineDb) {
console.log(chalk.gray("Offline mode:") + " " + chalk.yellow("enabled") + " " + chalk.gray("(no external advisory calls will be made)"));
}
console.log(`${chalk.gray("Advisory source:")} ${formatAdvisorySourceLine(advisorySourceLine)}`);
if (advisorySourceLine) {
console.log(`${chalk.gray("Advisory source:")} ${formatAdvisorySourceLine(advisorySourceLine)}`);
}
if (advisoryDbFreshnessLine) {
console.log(`${chalk.gray("Advisory DB freshness:")} ${advisoryDbFreshnessLine}`);
}
Expand Down Expand Up @@ -270,7 +276,8 @@ if (parsedArgs) {
projectPath,
debugLog,
});
const findingsBeforeFix = scanState.sorted.length;
const findingsBeforeFixList = scanState.sorted;
const findingsBeforeFix = findingsBeforeFixList.length;
let fixResult: FixExecutionResult | null = null;

if (options.fix) {
Expand Down Expand Up @@ -310,6 +317,30 @@ if (parsedArgs) {
findingsAfterFix: scanState.sorted.length,
remainingBySeverity: countBySeverity(scanState.sorted),
});

if (options.createPr && fixResult) {
if (fixResult.appliedFixCount === 0) {
logWarn("Skipping pull request creation: no direct fixes were applied.", options);
} else {
console.log("");
console.log(chalk.bold.cyan("Creating pull request (--create-pr)"));
const prResult = await createPullRequestForFixes({
projectPath,
baseBranch: options.prBase ?? "main",
fixResult,
findingsBeforeFix: findingsBeforeFixList,
findingsAfterFix: scanState.sorted,
});
if (prResult.skipped) {
logWarn(prResult.skipReason ?? "Pull request was not created.", options);
} else if (prResult.prUrl) {
console.log(`${chalk.gray("Pull request:")} ${chalk.cyan(prResult.prUrl)}`);
console.log(`${chalk.gray("Branch:")} ${chalk.cyan(prResult.branchName)}`);
} else {
logWarn(`Branch ${prResult.branchName} was pushed, but no pull request URL was returned.`, options);
}
}
}
} else {
await writeOutputs(options, {
sorted: scanState.sorted,
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ export type ParsedOptions = {
debug?: boolean;
verbose?: boolean;
fix?: boolean;
createPr?: boolean;
prBase?: string;
prodOnly?: boolean;
failOn: string;
batchSize: string;
Expand Down
Loading