#!/usr/bin/env node import path from "node:path"; import process from "node:process"; import { existsSync } from "node:fs"; import { parseArgs } from "./cli/args.js"; import { printBanner, printHelp, printOverridesHelp } from "./cli/help.js"; import { validateOptions } from "./cli/validate.js"; import { DEFAULT_BATCH_SIZE, DEFAULT_SEARCH_DEPTH } from "./constants.js"; import { chalk } from "./utils/chalk.js"; import { createDebugLogger } from "./output/debug.js"; import { getCliVersion } from "./utils/version-info.js"; import { getNetworkErrorHint } from "./utils/network.js"; import { nearestCommand } from "./utils/string.js"; import { EXIT_ERROR } from "./types.js"; import { formatHintLines, logWarn } from "./output/formatters.js"; import { installSkill } from "./skills/install.js"; import { setupCaCert } from "./cli/config.js"; import { handleConfigCommand } from "./cli/config-command.js"; import { createAuditLog } from "./audit-log/index.js"; import { hasRootLockfile, findNestedLockfiles } from "./parsers/multi-package.js"; import { handleMultiFolderScan } from "./scan/multi-folder-scan.js"; import { handleSingleFolderScan } from "./scan/single-scan.js"; let parsedArgs: ReturnType | null = null; try { parsedArgs = parseArgs(process.argv.slice(2)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Error: ${message}`)); console.error(chalk.gray("Run `cve-lite --help` to see supported options.")); process.exit(1); } if (parsedArgs) { const { command, options, projectArg, configSubcommand } = parsedArgs; const cliVersion = getCliVersion(); if (options.help) { if (command === "overrides") { printOverridesHelp(); } else { printHelp(); } process.exit(0); } else if (options.version) { printBanner(options); process.exit(0); } else { const projectPath = path.resolve(projectArg || "."); // A known command would have been dispatched before this branch, so an explicit // first argument that does not resolve to an existing path is almost certainly a // mistyped command (e.g. `cve-lite frobnicate`). Error instead of silently // scanning a nonexistent path and reporting "0 packages, exit 0" - a clean exit // on a typo'd command is a foot-gun for a security tool. Existing directories // still scan unchanged; the guard only fires on nonexistent paths. if (command === "scan" && projectArg && !existsSync(projectPath)) { const near = nearestCommand(projectArg); console.error( chalk.red( `Error: '${projectArg}' is not an existing path or a known command.` + (near ? ` Did you mean '${near}'?` : ""), ), ); console.error(chalk.gray("Run 'cve-lite --help' for usage.")); process.exit(EXIT_ERROR); } const batchSize = Number(options.batchSize || DEFAULT_BATCH_SIZE); const searchDepth = Math.max(0, Number(options.searchDepth || DEFAULT_SEARCH_DEPTH)); const debugSession = createDebugLogger(!!options.debug); const debugLog = debugSession.log; let scanJsonPath: string | undefined; const scanStartedAt = Date.now(); const auditLogHandle = createAuditLog(options.auditLog ?? process.env.CVE_LITE_AUDIT_LOG); async function main() { printBanner(options); debugSession.announcePath(); debugLog("CLI started", { version: cliVersion, args: process.argv.slice(2), }); if (command === "config") { handleConfigCommand(configSubcommand); process.exit(0); return; } const certFetch = setupCaCert(options, debugLog); if (command === "advisories-sync") { const { runAdvisoriesSync } = await import("./cli/advisories-sync.js"); await runAdvisoriesSync({ options, fetchImpl: certFetch }); process.exit(0); return; } if (command === "advisories-init") { const { runAdvisoriesInit } = await import("./cli/advisories-init.js"); runAdvisoriesInit(options); process.exit(0); return; } if (command === "install-skill") { installSkill(process.cwd()); process.exit(0); return; } if (command === "overrides") { const { runOverrides } = await import("./cli/overrides.js"); const logger = { info: (msg: string) => console.log(msg), warn: (msg: string) => console.warn(msg), error: (msg: string) => console.error(msg), debug: (msg: string) => console.debug(msg), }; const exitCode = await runOverrides({ projectArg, options, logger, auditLog: auditLogHandle, // Same cert-aware fetch and debug sink the scan path uses, so a CA certificate // and --debug behave identically on both. fetchImpl: certFetch, debugLog, }); auditLogHandle.close(); process.exit(exitCode); return; } const validationWarning = await validateOptions(options); if (validationWarning) { logWarn(validationWarning, options); } // Multi-folder mode: if no root lockfile and 2+ nested lockfiles exist, // route to dedicated multi-folder handler instead of single-lockfile scan const nestedLockfiles = findNestedLockfiles(projectPath, searchDepth); if (!hasRootLockfile(projectPath) && nestedLockfiles.length >= 2) { const exitCode = await handleMultiFolderScan({ projectRoot: projectPath, batchSize, options, fetchImpl: certFetch, auditLog: auditLogHandle, debugLog, onScanJsonPath: (jsonPath) => { scanJsonPath = jsonPath; }, }); debugSession.finalize(projectPath, options, scanJsonPath); auditLogHandle.close(); process.exit(exitCode); return; } const exitCode = await handleSingleFolderScan({ projectPath, projectArg, batchSize, searchDepth, options, debugLog, fetchImpl: certFetch, auditLog: auditLogHandle, scanStartedAt, cliVersion, onScanJsonPath: (jsonPath) => { scanJsonPath = jsonPath; }, }); debugSession.finalize(projectPath, options, scanJsonPath); auditLogHandle.close(); process.exit(exitCode); return; } main().catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Error: ${errorMessage}`)); debugLog("Unhandled error", { message: errorMessage, stack: error instanceof Error ? error.stack : undefined }); // Emit error event to audit-log auditLogHandle.emit({ ts: new Date().toISOString(), type: "error", schemaVersion: 1, phase: "scan", message: errorMessage, stack: error instanceof Error ? error.stack : undefined, }); debugSession.finalize(projectPath, options, scanJsonPath); auditLogHandle.close(); formatHintLines(getNetworkErrorHint(error, errorMessage)).forEach((line) => console.error(line), ); process.exit(1); }); } }