#!/usr/bin/env node
import { k as cliState_default, n as getLogLevel, o as logger_default } from "./logger-CjS9Mbik.js";
import { v as TERMINAL_MAX_WIDTH } from "./fetch-Brl8CXgW.js";
import "./cloud-CJaIuNs9.js";
import "./types-atpq0XCc.js";
import { t as printBorder } from "./util-BuhznZrR.js";
import "./esm-CNEnghzm.js";
import "./pythonUtils-Dxv9iWbf.js";
import { t as formatDuration } from "./formatDuration-CGa0ZDQ8.js";
import { z } from "zod";
import chalk from "chalk";
import fs from "fs";
import path, { isAbsolute, resolve } from "path";
import yaml from "js-yaml";
import crypto from "crypto";
import { spawn } from "child_process";
import async from "async";
import ora from "ora";
import simpleGit from "simple-git";
import binaryExtensions from "binary-extensions";
import { execa } from "execa";
import { isText } from "istextorbinary";
import textExtensions from "text-extensions";
import { minimatch } from "minimatch";
import { io } from "socket.io-client";

//#region src/types/codeScan.ts
const CodeScanSeverity = {
	CRITICAL: "critical",
	HIGH: "high",
	MEDIUM: "medium",
	LOW: "low",
	NONE: "none"
};
const CodeScanSeveritySchema = z.enum([
	"critical",
	"high",
	"medium",
	"low",
	"none"
]);
/**
* Get emoji representation for a severity level
* @param severity - The severity level
* @returns Emoji string representing the severity
*/
function getSeverityEmoji(severity) {
	switch (severity) {
		case CodeScanSeverity.CRITICAL: return "🔴";
		case CodeScanSeverity.HIGH: return "🟠";
		case CodeScanSeverity.MEDIUM: return "🟡";
		case CodeScanSeverity.LOW: return "🔵";
		case CodeScanSeverity.NONE: return "👍";
	}
}
/**
* Get numeric rank for a severity level (used for sorting)
* @param severity - The severity level
* @returns Numeric rank (higher = more severe)
*/
function getSeverityRank(severity) {
	switch (severity) {
		case CodeScanSeverity.CRITICAL: return 4;
		case CodeScanSeverity.HIGH: return 3;
		case CodeScanSeverity.MEDIUM: return 2;
		case CodeScanSeverity.LOW: return 1;
		case CodeScanSeverity.NONE: return -1;
	}
}
/**
* Format severity level for display
* @param severity - The severity level (optional, returns empty string if undefined)
* @param style - Display style ('plain' or 'markdown')
* @returns Formatted severity string
*/
function formatSeverity(severity, style = "plain") {
	if (!severity) return "";
	const emoji = getSeverityEmoji(severity);
	const displayText = severity === CodeScanSeverity.NONE ? "All Clear" : capitalize(severity);
	if (style === "markdown") return `_${emoji} ${displayText}_\n\n`;
	return `${emoji} ${displayText}`;
}
/**
* Count comments by severity level
* @param comments - Array of comments with severity property (optional severity)
* @returns Object with counts for each severity level
*/
function countBySeverity(comments) {
	const validSeverities = [
		CodeScanSeverity.CRITICAL,
		CodeScanSeverity.HIGH,
		CodeScanSeverity.MEDIUM,
		CodeScanSeverity.LOW
	];
	const issuesOnly = comments.filter((c) => c.severity && validSeverities.includes(c.severity));
	return {
		total: issuesOnly.length,
		critical: issuesOnly.filter((c) => c.severity === CodeScanSeverity.CRITICAL).length,
		high: issuesOnly.filter((c) => c.severity === CodeScanSeverity.HIGH).length,
		medium: issuesOnly.filter((c) => c.severity === CodeScanSeverity.MEDIUM).length,
		low: issuesOnly.filter((c) => c.severity === CodeScanSeverity.LOW).length
	};
}
/**
* Helper function to capitalize the first letter of a string
*/
function capitalize(str) {
	return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Validates and parses a severity string into CodeScanSeverity enum
* Normalizes input by trimming whitespace and converting to lowercase
* @param severity - String input to validate (e.g., "high", "CRITICAL", " medium ")
* @returns Validated CodeScanSeverity enum value
* @throws {z.ZodError} if severity is not a valid CodeScanSeverity value
* @example
* validateSeverity('high') // Returns CodeScanSeverity.HIGH
* validateSeverity('CRITICAL') // Returns CodeScanSeverity.CRITICAL
* validateSeverity('invalid') // Throws ZodError
*/
function validateSeverity(severity) {
	const normalized = severity.trim().toLowerCase();
	return CodeScanSeveritySchema.parse(normalized);
}
/**
* Schema for a valid line range in a file's diff.
* Used for validating PR comment line numbers.
*/
const LineRangeSchema = z.object({
	start: z.number(),
	end: z.number()
});
const FileRecordSchema = z.object({
	path: z.string(),
	status: z.string(),
	shaA: z.string().nullable(),
	shaB: z.string().nullable(),
	linesAdded: z.number().optional(),
	linesRemoved: z.number().optional(),
	beforeSizeBytes: z.number().optional(),
	afterSizeBytes: z.number().optional(),
	isText: z.boolean().optional(),
	skipReason: z.string().optional(),
	patch: z.string().optional(),
	lineRanges: z.array(LineRangeSchema).optional()
});
const GitMetadataSchema = z.object({
	branch: z.string(),
	baseBranch: z.string(),
	baseRef: z.string(),
	baseSha: z.string(),
	compareRef: z.string(),
	compareSha: z.string(),
	commitMessages: z.array(z.string()),
	author: z.string(),
	timestamp: z.string()
});
const ScanConfigSchema = z.object({
	minimumSeverity: CodeScanSeveritySchema,
	diffsOnly: z.boolean(),
	guidance: z.string().optional()
});
const PullRequestContextSchema = z.object({
	owner: z.string(),
	repo: z.string(),
	number: z.number(),
	sha: z.string()
});
const ScanRequestSchema = z.object({
	files: z.array(FileRecordSchema).min(1, "Files array cannot be empty"),
	metadata: GitMetadataSchema,
	config: ScanConfigSchema,
	sessionId: z.string(),
	pullRequest: PullRequestContextSchema.optional()
});
const CommentSchema = z.object({
	file: z.string().nullable(),
	startLine: z.number().nullable().optional(),
	line: z.number().nullable(),
	finding: z.string(),
	fix: z.string().nullable().optional(),
	severity: CodeScanSeveritySchema.optional(),
	aiAgentPrompt: z.string().nullable().optional()
});
const PhaseResultsSchema = z.object({
	inventory: z.string(),
	tracing: z.string(),
	analysis: z.string(),
	filtering: z.string(),
	fixes: z.string(),
	comments: z.string()
});
const ScanResponseSchema = z.object({
	success: z.boolean(),
	review: z.string().optional(),
	comments: z.array(CommentSchema),
	commentsPosted: z.boolean().optional(),
	batchCount: z.number().optional(),
	error: z.string().optional()
});
/**
* Error thrown when git operations fail
*/
var GitError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "GitError";
	}
};
/**
* Error thrown when git metadata extraction fails
*/
var GitMetadataError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "GitMetadataError";
	}
};
/**
* Error thrown when diff processing fails
*/
var DiffProcessorError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "DiffProcessorError";
	}
};
/**
* Error thrown when MCP filesystem server startup fails
*/
var FilesystemMcpError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "FilesystemMcpError";
	}
};
/**
* Error thrown when Socket.io MCP bridge connection fails
*/
var SocketIoMcpBridgeError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "SocketIoMcpBridgeError";
	}
};
/**
* Error thrown when config file loading or parsing fails
*/
var ConfigLoadError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "ConfigLoadError";
	}
};

//#endregion
//#region src/codeScan/config/schema.ts
/**
* Configuration Schema
*
* Zod schema for validating YAML configuration files.
*/
const ConfigSchema = z.object({
	minSeverity: CodeScanSeveritySchema.optional().describe("Minimum severity level for reporting vulnerabilities"),
	minimumSeverity: CodeScanSeveritySchema.optional().describe("Alias for minSeverity"),
	diffsOnly: z.boolean().default(false).describe("Only scan PR diffs, skip filesystem exploration (default: explore full repo)"),
	apiHost: z.string().optional().describe("Scan server URL (default: https://api.promptfoo.app)"),
	guidance: z.string().optional().describe("Custom guidance for the security scan"),
	guidanceFile: z.string().optional().describe("Path to file containing custom guidance")
}).refine((data) => !(data.guidance && data.guidanceFile), { message: "Cannot specify both guidance and guidanceFile" }).transform((data) => {
	const minimumSeverity = data.minSeverity ?? data.minimumSeverity ?? CodeScanSeverity.MEDIUM;
	const { minimumSeverity: _, ...rest } = data;
	return {
		...rest,
		minimumSeverity
	};
});
const DEFAULT_CONFIG = {
	minimumSeverity: CodeScanSeverity.MEDIUM,
	diffsOnly: false
};

//#endregion
//#region src/codeScan/config/loader.ts
/**
* Configuration Loader
*
* Loads and validates configuration from YAML files.
*/
/**
* Load configuration from a YAML file
* @param configPath Path to the configuration file
* @returns Validated configuration object
* @throws ConfigLoadError if file cannot be read or parsed
*/
function loadConfig(configPath) {
	if (!fs.existsSync(configPath)) throw new ConfigLoadError(`Configuration file not found: ${configPath}`);
	let fileContents;
	try {
		fileContents = fs.readFileSync(configPath, "utf8");
	} catch (error) {
		throw new ConfigLoadError(`Failed to read configuration file: ${error instanceof Error ? error.message : String(error)}`);
	}
	let rawConfig;
	try {
		rawConfig = yaml.load(fileContents);
	} catch (error) {
		throw new ConfigLoadError(`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`);
	}
	const result = ConfigSchema.safeParse(rawConfig);
	if (!result.success) throw new ConfigLoadError(`Invalid configuration: ${result.error.errors.map((err) => `${err.path.join(".")}: ${err.message}`).join(", ")}`);
	const config = result.data;
	if (config.guidanceFile) {
		const guidanceFilePath = path.isAbsolute(config.guidanceFile) ? config.guidanceFile : path.resolve(path.dirname(configPath), config.guidanceFile);
		try {
			config.guidance = fs.readFileSync(guidanceFilePath, "utf-8");
		} catch (error) {
			throw new ConfigLoadError(`Failed to read guidance file "${guidanceFilePath}": ${error instanceof Error ? error.message : String(error)}`);
		}
	}
	return config;
}
/**
* Load configuration with defaults
* If no config path provided, returns default configuration
* Returns a clone to prevent mutation of the singleton default
*/
function loadConfigOrDefault(configPath) {
	if (!configPath) return { ...DEFAULT_CONFIG };
	return loadConfig(configPath);
}
/**
* Merge CLI options with configuration
*
* CLI options take precedence over config file settings
*
* @param config - Base configuration
* @param options - CLI options to merge
* @returns Merged configuration
*/
function mergeConfigWithOptions(config, options) {
	const merged = { ...config };
	if (options.diffsOnly !== void 0) merged.diffsOnly = options.diffsOnly;
	if (options.minSeverity || options.minimumSeverity) merged.minimumSeverity = validateSeverity(options.minSeverity || options.minimumSeverity);
	if (options.apiHost) merged.apiHost = options.apiHost;
	return merged;
}
/**
* Resolve guidance from options or config
*
* CLI options take precedence over config file settings
*
* @param options - Options that may contain guidance
* @param config - Configuration that may contain guidance
* @returns Guidance string or undefined
* @throws Error if both guidance and guidanceFile are specified
*/
function resolveGuidance(options, config) {
	if (options.guidance && options.guidanceFile) throw new Error("Cannot specify both --guidance and --guidance-file options");
	if (options.guidance) return options.guidance;
	if (options.guidanceFile) {
		const absoluteGuidancePath = path.resolve(options.guidanceFile);
		try {
			return fs.readFileSync(absoluteGuidancePath, "utf-8");
		} catch (error) {
			throw new Error(`Failed to read guidance file: ${absoluteGuidancePath} - ${error instanceof Error ? error.message : String(error)}`);
		}
	}
	return config.guidance;
}
/**
* Resolve API host from options or config
*
* @param options - Options that may contain API host
* @param config - Configuration that may contain API host
* @returns API host URL
*/
function resolveApiHost(options, config) {
	return options.apiHost || config.apiHost || "https://api.promptfoo.app";
}

//#endregion
//#region src/codeScan/git/diff.ts
/**
* Validate that we're on a branch (not detached HEAD)
* @param git Simple git instance
* @throws GitError if not on a branch
*/
async function validateOnBranch(git) {
	try {
		const status = await git.status();
		if (status.detached) throw new GitError("Not on a branch. Please checkout a branch before running the scan.");
		if (!status.current) throw new GitError("Could not determine current branch.");
		return status.current;
	} catch (error) {
		if (error instanceof GitError) throw error;
		throw new GitError(`Failed to validate branch: ${error instanceof Error ? error.message : String(error)}`);
	}
}

//#endregion
//#region src/codeScan/constants/filtering.ts
/**
* File Filtering Constants
*
* Shared constants for code scan file filtering used by both CLI and server.
* Extracted to avoid pulling in ESM dependencies (like execa) into server tests.
*/
const DENYLIST_PATTERNS = [
	"**/node_modules/**",
	"**/dist/**",
	"**/build/**",
	"**/.next/**",
	"**/.venv/**",
	"**/__pycache__/**",
	"**/*.lock",
	"**/package-lock.json",
	"**/yarn.lock",
	"**/pnpm-lock.yaml",
	"**/Cargo.lock",
	"**/poetry.lock",
	"**/composer.lock",
	"**/Pipfile.lock",
	"**/*.min.js",
	"**/*.map",
	"**/*.bin",
	"**/*.exe",
	"**/*.dll",
	"**/*.so",
	"**/*.dylib",
	"**/*.zip",
	"**/*.tar",
	"**/*.gz",
	"**/*.jpg",
	"**/*.jpeg",
	"**/*.png",
	"**/*.gif",
	"**/*.pdf",
	"**/*.mp4",
	"**/*.mov"
];
const MAX_BLOB_SIZE_BYTES = 500 * 1024;
const MAX_PATCH_SIZE_BYTES = 200 * 1024;
function isInDenylist(filePath) {
	return DENYLIST_PATTERNS.some((pattern) => minimatch(filePath, pattern));
}

//#endregion
//#region src/codeScan/util/diffHunkParser.ts
/**
* Regular expression to match unified diff hunk headers.
*
* Format: @@ -oldStart[,oldCount] +newStart[,newCount] @@
* The count is optional and defaults to 1 if not present.
*
* Examples:
* - @@ -10,7 +20,8 @@ -> old: 10,7  new: 20,8
* - @@ -1 +1 @@        -> old: 1,1  new: 1,1
* - @@ -0,0 +1,5 @@    -> old: 0,0  new: 1,5 (new file)
*/
const HUNK_HEADER_REGEX = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
/**
* Parse a unified diff hunk header line.
*
* @param line - A line that may be a hunk header
* @returns Parsed hunk information, or null if not a hunk header
*
* @example
* ```typescript
* parseHunkHeader('@@ -10,7 +20,8 @@ function foo() {')
* // Returns: { oldStart: 10, oldCount: 7, newStart: 20, newCount: 8 }
*
* parseHunkHeader('regular code line')
* // Returns: null
* ```
*/
function parseHunkHeader(line) {
	const match = line.match(HUNK_HEADER_REGEX);
	if (!match) return null;
	return {
		oldStart: parseInt(match[1], 10),
		oldCount: match[2] ? parseInt(match[2], 10) : 1,
		newStart: parseInt(match[3], 10),
		newCount: match[4] ? parseInt(match[4], 10) : 1
	};
}

//#endregion
//#region src/codeScan/git/diffAnnotator.ts
/**
* Diff Annotator
*
* Adds absolute line numbers to unified diff format for easier LLM processing.
* This eliminates the need for LLMs to manually calculate line numbers from hunk headers.
*
* Also extracts valid line ranges for each file, which can be used to validate
* and clamp comment line numbers for GitHub PR reviews.
*/
/**
* Annotate a unified diff patch with absolute line numbers and extract valid line ranges.
*/
function annotateDiffWithLineRanges(patch) {
	if (!patch || patch.trim() === "") return {
		annotatedDiff: patch,
		lineRanges: []
	};
	const lines = patch.split("\n");
	const result = [];
	const lineRanges = [];
	let currentNewLine = 0;
	let inHunk = false;
	let hunkStartLine = 0;
	for (let i = 0; i < lines.length; i++) {
		const line = lines[i];
		const isLastLine = i === lines.length - 1;
		const hunkHeader = parseHunkHeader(line);
		if (hunkHeader) {
			if (hunkStartLine > 0 && currentNewLine > hunkStartLine) lineRanges.push({
				start: hunkStartLine,
				end: currentNewLine - 1
			});
			currentNewLine = hunkHeader.newStart;
			inHunk = true;
			hunkStartLine = hunkHeader.newCount === 0 ? 0 : hunkHeader.newStart;
			result.push(line);
			continue;
		}
		if (!inHunk) {
			result.push(line);
			continue;
		}
		if (line.startsWith("-")) result.push(line);
		else if (line.startsWith("+")) {
			result.push(`L${currentNewLine}: ${line}`);
			currentNewLine++;
		} else if (line.startsWith(" ")) {
			result.push(`L${currentNewLine}: ${line}`);
			currentNewLine++;
		} else if (line.startsWith("\\")) result.push(line);
		else if (line === "" && isLastLine) result.push(line);
		else {
			result.push(`L${currentNewLine}: ${line}`);
			currentNewLine++;
		}
	}
	if (hunkStartLine > 0 && currentNewLine > hunkStartLine) lineRanges.push({
		start: hunkStartLine,
		end: currentNewLine - 1
	});
	return {
		annotatedDiff: result.join("\n"),
		lineRanges
	};
}

//#endregion
//#region src/codeScan/git/diffProcessor.ts
/**
* Diff Processor
*
* Focused pipeline for processing git diffs:
* 1. Discover changed files
* 2. Filter denylist (early exit)
* 3. Collect blob sizes and filter large files
* 4. Determine text/binary status
* 5. Generate per-file patches
*/
const PATCH_CONCURRENCY = 8;
const TEXT_DETECTION_CONCURRENCY = 16;
/**
* Parse git diff --raw -z output
*
* Format for normal operations (M, A, D):
*   :oldmode newmode oldsha newsha status\0path\0
*
* Format for renames/copies (R, C):
*   :oldmode newmode oldsha newsha status\0oldpath\0newpath\0
*
* Note: Rename/Copy status includes similarity (e.g., R100, R90, C100)
*/
function parseRawDiff(rawOutput) {
	const entries = rawOutput.split("\0").filter(Boolean);
	const results = [];
	let i = 0;
	while (i < entries.length) {
		const metaLine = entries[i];
		i++;
		if (!metaLine || i >= entries.length) break;
		const parts = metaLine.trim().split(/\s+/);
		if (parts.length < 5) continue;
		const shaA = parts[2] === "0000000000000000000000000000000000000000" ? null : parts[2];
		const shaB = parts[3] === "0000000000000000000000000000000000000000" ? null : parts[3];
		const status = parts[4];
		if (status.startsWith("R") || status.startsWith("C")) {
			const oldPath = entries[i];
			i++;
			const newPath = entries[i];
			i++;
			if (!oldPath || !newPath) continue;
			results.push({
				path: newPath,
				oldPath,
				status,
				shaA,
				shaB
			});
		} else {
			const filePath = entries[i];
			i++;
			if (!filePath) continue;
			results.push({
				path: filePath,
				status,
				shaA,
				shaB
			});
		}
	}
	return results;
}
/**
* Parse git diff --numstat output
* Format: added\tremoved\tpath
*/
function parseNumstat(numstatOutput) {
	const map = /* @__PURE__ */ new Map();
	for (const line of numstatOutput.split("\n")) {
		if (!line.trim()) continue;
		const parts = line.split("	");
		if (parts.length < 3) continue;
		const added = parts[0] === "-" ? 0 : Number.parseInt(parts[0], 10);
		const removed = parts[1] === "-" ? 0 : Number.parseInt(parts[1], 10);
		const path$1 = parts[2];
		map.set(path$1, {
			linesAdded: added,
			linesRemoved: removed
		});
	}
	return map;
}
async function discoverChangedFiles(repoPath, base, compare) {
	const [rawResult, numstatResult] = await Promise.all([execa("git", [
		"diff",
		"--raw",
		"-z",
		"--no-color",
		"--no-ext-diff",
		"--no-abbrev",
		`${base}...${compare}`
	], { cwd: repoPath }), execa("git", [
		"diff",
		"--numstat",
		`${base}...${compare}`
	], { cwd: repoPath })]);
	const rawFiles = parseRawDiff(rawResult.stdout);
	const numstatMap = parseNumstat(numstatResult.stdout);
	return rawFiles.map((file) => {
		const stats = numstatMap.get(file.path);
		return {
			path: file.path,
			status: file.status,
			shaA: file.shaA,
			shaB: file.shaB,
			linesAdded: stats?.linesAdded,
			linesRemoved: stats?.linesRemoved
		};
	});
}
function filterDenylist(files) {
	return files.map((file) => {
		if (isInDenylist(file.path)) return {
			...file,
			skipReason: "denylist"
		};
		return file;
	});
}
async function collectBlobSizes(repoPath, files) {
	const shas = /* @__PURE__ */ new Set();
	for (const file of files) {
		if (file.skipReason) continue;
		if (file.shaA) shas.add(file.shaA);
		if (file.shaB) shas.add(file.shaB);
	}
	if (shas.size === 0) return /* @__PURE__ */ new Map();
	const result = await execa("git", ["cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], {
		cwd: repoPath,
		input: Array.from(shas).join("\n")
	});
	const sizeMap = /* @__PURE__ */ new Map();
	for (const line of result.stdout.split("\n")) {
		if (!line.trim()) continue;
		const parts = line.split(/\s+/);
		if (parts.length < 3) continue;
		const sha = parts[0];
		const size = Number.parseInt(parts[2], 10);
		sizeMap.set(sha, size);
	}
	return sizeMap;
}
function attachBlobSizesAndFilter(files, sizeMap) {
	return files.map((file) => {
		if (file.skipReason) return file;
		const beforeSize = file.shaA ? sizeMap.get(file.shaA) : void 0;
		const afterSize = file.shaB ? sizeMap.get(file.shaB) : void 0;
		if (beforeSize !== void 0 && beforeSize > MAX_BLOB_SIZE_BYTES || afterSize !== void 0 && afterSize > MAX_BLOB_SIZE_BYTES) return {
			...file,
			beforeSizeBytes: beforeSize,
			afterSizeBytes: afterSize,
			skipReason: "too large"
		};
		return {
			...file,
			beforeSizeBytes: beforeSize,
			afterSizeBytes: afterSize
		};
	});
}
async function isBlobText(repoPath, sha) {
	try {
		const result = await execa("git", [
			"cat-file",
			"blob",
			sha
		], {
			cwd: repoPath,
			encoding: "buffer",
			maxBuffer: 4096
		});
		return isText(null, Buffer.from(result.stdout)) === true;
	} catch {
		return false;
	}
}
/**
* Check file extension against known text and binary extension lists
* @returns 'text' if known text extension, 'binary' if known binary extension, 'unknown' otherwise
*/
function getExtensionType(filePath) {
	const ext = path.extname(filePath).toLowerCase().slice(1);
	if (textExtensions.includes(ext)) return "text";
	if (binaryExtensions.includes(ext)) return "binary";
	return "unknown";
}
/**
* Determine text/binary status for a single file
* Uses 2-tier approach: extension lists first, then blob content analysis for unknown extensions
*/
async function determineTextStatusForFile(repoPath, file) {
	if (file.skipReason) return file;
	const extensionType = getExtensionType(file.path);
	if (extensionType === "text") return {
		...file,
		isText: true
	};
	if (extensionType === "binary") return {
		...file,
		isText: false,
		skipReason: "binary"
	};
	const checkSha = file.shaB || file.shaA;
	if (!checkSha) return {
		...file,
		isText: false,
		skipReason: "binary"
	};
	if (await isBlobText(repoPath, checkSha)) return {
		...file,
		isText: true
	};
	else return {
		...file,
		isText: false,
		skipReason: "binary"
	};
}
async function determineTextStatus(repoPath, files) {
	return async.mapLimit(files, TEXT_DETECTION_CONCURRENCY, async (file) => determineTextStatusForFile(repoPath, file));
}
async function generatePatchForFile(repoPath, base, compare, filePath) {
	try {
		const patch = (await execa("git", [
			"diff",
			"--patch",
			"--unified=3",
			"--no-color",
			"--no-ext-diff",
			`${base}...${compare}`,
			"--",
			filePath
		], {
			cwd: repoPath,
			maxBuffer: MAX_PATCH_SIZE_BYTES
		})).stdout;
		if (Buffer.byteLength(patch, "utf8") > MAX_PATCH_SIZE_BYTES) return {
			success: false,
			skipReason: "patch too large"
		};
		const { annotatedDiff, lineRanges } = annotateDiffWithLineRanges(patch);
		return {
			success: true,
			patch: annotatedDiff,
			lineRanges
		};
	} catch (err) {
		const errorMessage = err instanceof Error ? err.message : String(err);
		if (errorMessage.includes("maxBuffer") || errorMessage.includes("stdout maxBuffer")) {
			logger_default.debug(`git diff --patch ${filePath} exceeded maxBuffer (${MAX_PATCH_SIZE_BYTES} bytes) - patch too large`);
			return {
				success: false,
				skipReason: "patch too large"
			};
		}
		logger_default.debug(`git diff --patch ${filePath} failed: ${errorMessage} - skipping file`);
		return {
			success: false,
			skipReason: "diff error"
		};
	}
}
async function generatePatches(repoPath, base, compare, files) {
	return async.mapLimit(files, PATCH_CONCURRENCY, async (file) => {
		if (file.skipReason) return file;
		const result = await generatePatchForFile(repoPath, base, compare, file.path);
		if (!result.success) return {
			...file,
			skipReason: result.skipReason
		};
		return {
			...file,
			patch: result.patch,
			lineRanges: result.lineRanges
		};
	});
}
async function processDiff(repoPath, base, compare = "HEAD") {
	try {
		let files = await discoverChangedFiles(repoPath, base, compare);
		if (files.length === 0) return files;
		files = filterDenylist(files);
		if (files.filter((f) => !f.skipReason).length === 0) return files;
		const sizeMap = await collectBlobSizes(repoPath, files);
		files = attachBlobSizesAndFilter(files, sizeMap);
		if (files.filter((f) => !f.skipReason).length === 0) return files;
		files = await determineTextStatus(repoPath, files);
		if (files.filter((f) => !f.skipReason).length === 0) return files;
		files = await generatePatches(repoPath, base, compare, files);
		if (files.filter((f) => !f.skipReason && f.patch).length === 0) return files;
		return files;
	} catch (error) {
		if (error instanceof DiffProcessorError) throw error;
		throw new DiffProcessorError(`Failed to process diff: ${error instanceof Error ? error.message : String(error)}`);
	}
}

//#endregion
//#region src/codeScan/git/metadata.ts
/**
* Git Metadata Extraction
*
* Extracts metadata about the current branch and commits.
*/
/**
* Extract git metadata for the comparison
* @param repoPath Path to the git repository
* @param baseBranch Base branch or commit
* @param compareRef Compare branch or commit
* @returns Git metadata object
*/
async function extractMetadata(repoPath, baseBranch, compareRef) {
	const git = simpleGit(repoPath);
	try {
		const baseRef = baseBranch;
		const compareRefValue = compareRef;
		const baseSha = (await git.revparse([baseBranch])).trim();
		const compareSha = (await git.revparse([compareRef])).trim();
		const log = await git.log({
			from: baseBranch,
			to: compareRef
		});
		return {
			branch: compareRef,
			baseBranch,
			baseRef,
			baseSha,
			compareRef: compareRefValue,
			compareSha,
			commitMessages: log.all.map((commit) => {
				return `${commit.hash.substring(0, 7)}: ${commit.message}`;
			}),
			author: log.latest?.author_name || "Unknown",
			timestamp: log.latest?.date || (/* @__PURE__ */ new Date()).toISOString()
		};
	} catch (error) {
		throw new GitMetadataError(`Failed to extract git metadata: ${error instanceof Error ? error.message : String(error)}`);
	}
}

//#endregion
//#region src/codeScan/mcp/filesystem.ts
/**
* Filesystem MCP Server Management
*
* Spawns and manages the @modelcontextprotocol/server-filesystem child process.
*/
/**
* Start the filesystem MCP server as a child process
* @param rootDir Absolute path to root directory for filesystem access
* @returns Child process handle
*/
function startFilesystemMcpServer(rootDir) {
	if (!isAbsolute(rootDir)) throw new FilesystemMcpError(`Root directory must be an absolute path, got: ${rootDir}`);
	const absoluteRootDir = resolve(rootDir);
	logger_default.debug("Starting filesystem MCP server...");
	logger_default.debug(`Root directory: ${absoluteRootDir}`);
	try {
		const mcpProcess = spawn("npx", [
			"-y",
			"@modelcontextprotocol/server-filesystem",
			absoluteRootDir
		], {
			stdio: [
				"pipe",
				"pipe",
				"pipe"
			],
			cwd: absoluteRootDir
		});
		mcpProcess.stderr?.on("data", (chunk) => {
			const message = chunk.toString("utf8");
			if (message.includes("Failed to request initial roots from client")) return;
			logger_default.debug(`MCP server stderr: ${message.trim()}`);
		});
		mcpProcess.on("error", (error) => {
			logger_default.error(`MCP server process error: ${error.message}`);
		});
		mcpProcess.on("exit", (code, signal) => {
			if (code !== null && code !== 0) logger_default.debug(`MCP server exited with code ${code}`);
			else if (signal) logger_default.debug(`MCP server terminated by signal ${signal}`);
		});
		logger_default.debug(`MCP server started (pid: ${mcpProcess.pid})`);
		return mcpProcess;
	} catch (error) {
		throw new FilesystemMcpError(`Failed to start filesystem MCP server: ${error instanceof Error ? error.message : String(error)}`);
	}
}
/**
* Stop the filesystem MCP server process
* @param process Child process to terminate
*/
async function stopFilesystemMcpServer(process$1) {
	if (!process$1.pid) {
		logger_default.debug("MCP server already stopped");
		return;
	}
	logger_default.debug(`Stopping MCP server (pid: ${process$1.pid})...`);
	return new Promise((resolve$1) => {
		const timeout = setTimeout(() => {
			logger_default.debug("MCP server did not exit gracefully, force killing...");
			process$1.kill("SIGKILL");
			resolve$1();
		}, 5e3);
		process$1.on("exit", () => {
			clearTimeout(timeout);
			logger_default.debug("MCP server stopped");
			resolve$1();
		});
		process$1.kill("SIGTERM");
	});
}

//#endregion
//#region src/codeScan/mcp/transport.ts
/**
* Bridge between MCP stdio and socket.io
*/
var SocketIoMcpBridge = class {
	readBuffer = "";
	wireIdSeq = 0;
	wireIdMap = /* @__PURE__ */ new Map();
	constructor(mcpProcess, socket, sessionId) {
		this.mcpProcess = mcpProcess;
		this.socket = socket;
		this.sessionId = sessionId;
	}
	/**
	* Start bridging MCP stdio with the provided socket
	*/
	async connect() {
		if (!this.socket.connected) throw new SocketIoMcpBridgeError("Socket must be connected before starting bridge");
		logger_default.debug(`Using existing socket connection (id: ${this.socket.id})`);
		this.startBridging();
	}
	/**
	* Start bridging MCP stdio and socket.io
	*/
	startBridging() {
		if (!this.socket) throw new SocketIoMcpBridgeError("Socket not initialized");
		this.mcpProcess.stdout?.on("data", (chunk) => {
			this.readBuffer += chunk.toString("utf8");
			while (true) {
				const newlineIndex = this.readBuffer.indexOf("\n");
				if (newlineIndex === -1) break;
				const line = this.readBuffer.slice(0, newlineIndex);
				this.readBuffer = this.readBuffer.slice(newlineIndex + 1);
				if (!line.trim()) continue;
				try {
					const message = JSON.parse(line);
					const isResponse = "result" in message || "error" in message;
					const isRequest = "method" in message && !isResponse;
					let batchId;
					let restoredMessage = message;
					if (isResponse && message.id !== void 0) {
						const wireId = String(message.id);
						const mapping = this.wireIdMap.get(wireId);
						if (mapping) {
							batchId = mapping.batchId;
							restoredMessage = {
								...message,
								id: mapping.originalId
							};
							this.wireIdMap.delete(wireId);
						} else batchId = 0;
					}
					if (isResponse) this.socket?.emit("mcp:response", {
						session_id: this.sessionId,
						batch_id: batchId ?? 0,
						message: restoredMessage
					});
					else if (isRequest) this.socket?.emit("mcp:server-request", {
						session_id: this.sessionId,
						batch_id: batchId ?? 0,
						message
					});
					else this.socket?.emit("mcp:server-notification", {
						session_id: this.sessionId,
						batch_id: batchId ?? 0,
						message
					});
				} catch (_error) {
					logger_default.debug(`Failed to parse MCP output: ${line}`);
				}
			}
		});
		this.socket.on("mcp:request", (message) => {
			try {
				const batchId = message._batch_id ?? 0;
				const { _batch_id, ...cleanMessage } = message;
				let messageToSend = cleanMessage;
				if (cleanMessage.id !== void 0 && cleanMessage.id !== null) {
					const seq = this.wireIdSeq++;
					const wireId = `b${batchId}:${String(cleanMessage.id)}:${seq}`;
					this.wireIdMap.set(wireId, {
						batchId,
						originalId: cleanMessage.id
					});
					messageToSend = {
						...cleanMessage,
						id: wireId
					};
				}
				const jsonLine = JSON.stringify(messageToSend) + "\n";
				this.mcpProcess.stdin?.write(jsonLine);
			} catch (error) {
				logger_default.error(`Failed to write to MCP stdin: ${error instanceof Error ? error.message : String(error)}`);
			}
		});
		logger_default.debug("MCP ↔ Socket.io bridge active");
	}
	/**
	* Stop bridging (socket lifecycle managed externally)
	*/
	async disconnect() {
		logger_default.debug("Stopping MCP bridge...");
		this.wireIdMap.clear();
		this.wireIdSeq = 0;
		logger_default.debug("MCP bridge stopped");
	}
	/**
	* Get socket ID (if connected)
	*/
	get socketId() {
		return this.socket?.id || null;
	}
	/**
	* Check if currently connected
	*/
	get connected() {
		return this.socket?.connected === true;
	}
};

//#endregion
//#region src/codeScan/mcp/index.ts
/**
* Set up MCP bridge for filesystem access
*
* Creates a filesystem MCP server, bridges it with Socket.IO, and announces
* the runner to the server with the session ID and repository root.
*
* @param socket - Connected Socket.IO socket
* @param absoluteRepoPath - Absolute path to repository root
* @param sessionId - Session ID to use (generated by caller)
* @returns MCP bridge setup result
*/
async function setupMcpBridge(socket, absoluteRepoPath, sessionId) {
	logger_default.debug("Setting up repo MCP access...");
	logger_default.debug(`Using session ID: ${sessionId}`);
	const mcpProcess = startFilesystemMcpServer(absoluteRepoPath);
	const mcpBridge = new SocketIoMcpBridge(mcpProcess, socket, sessionId);
	await mcpBridge.connect();
	socket.emit("runner:hello", {
		session_id: sessionId,
		repo_root: absoluteRepoPath
	});
	return {
		mcpProcess,
		mcpBridge,
		sessionId
	};
}

//#endregion
//#region src/codeScan/util/auth.ts
/**
* Shared authentication utilities
* Reusable across HTTP requests and socket.io connections
*/
let cloudConfig;
try {
	cloudConfig = (await import("./cloud-H0lMbD0S.js")).cloudConfig;
} catch (_error) {}
/**
* Resolve authentication credentials using waterfall approach:
* 1. API key from CLI argument or config file (passed as parameter)
* 2. PROMPTFOO_API_KEY environment variable
* 3. API key from promptfoo auth (cloudConfig)
* 4. GitHub OIDC token (environment variable)
* 5. Fork PR context (for fork PRs where OIDC is unavailable)
*
* @param apiKey Optional API key from CLI arg or config file
* @param forkPR Optional fork PR context for authentication
* @returns Resolved auth credentials
*/
function resolveAuthCredentials(apiKey, forkPR) {
	if (apiKey) {
		logger_default.debug("Using API key from CLI/config");
		return { apiKey };
	}
	const envApiKey = process.env.PROMPTFOO_API_KEY;
	if (envApiKey) {
		logger_default.debug("Using API key from PROMPTFOO_API_KEY env var");
		return { apiKey: envApiKey };
	}
	if (cloudConfig) {
		const storedApiKey = cloudConfig.getApiKey();
		if (storedApiKey) {
			logger_default.debug("Using API key from promptfoo auth");
			return { apiKey: storedApiKey };
		}
	}
	const oidcToken = process.env.GITHUB_OIDC_TOKEN;
	if (oidcToken) {
		logger_default.debug("Using GitHub OIDC token");
		return { oidcToken };
	}
	if (forkPR) {
		logger_default.debug("Using fork PR context for authentication");
		return { forkPR };
	}
	return {};
}

//#endregion
//#region src/codeScan/util/github.ts
/**
* Parse GitHub PR string
*
* @param prString - GitHub PR string in format: owner/repo#number (e.g., promptfoo/promptfoo#123)
* @returns Parsed PR object or null if invalid format
*/
function parseGitHubPr(prString) {
	const match = prString.match(/^([^/]+)\/([^#]+)#(\d+)$/);
	if (!match) return null;
	const [, owner, repo, prNumber] = match;
	return {
		owner,
		repo,
		number: parseInt(prNumber, 10)
	};
}

//#endregion
//#region src/codeScan/scanner/cleanup.ts
/**
* Register cleanup handlers for process signals
*
* Handles SIGINT (Ctrl+C), SIGTERM, and SIGQUIT signals to ensure
* graceful shutdown of resources (socket, MCP bridge, spinner).
*
* @param refs - Mutable references to resources that need cleanup
*/
function registerCleanupHandlers(refs) {
	const cleanup = (signal) => {
		logger_default.debug(`Received ${signal}, cleaning up...`);
		if (refs.abortController) refs.abortController.abort();
	};
	process.once("SIGINT", () => cleanup("SIGINT"));
	process.once("SIGTERM", () => cleanup("SIGTERM"));
	process.once("SIGQUIT", () => cleanup("SIGQUIT"));
}

//#endregion
//#region src/codeScan/scanner/output.ts
/**
* Output Formatting and Display
*
* Handles formatting and displaying scan results in various formats (JSON, pretty-print).
*/
/**
* Create spinner if appropriate for the current environment
*
* @param options - Options for spinner creation
* @returns Spinner instance or undefined if spinner should not be shown
*/
function createSpinner(options) {
	if (!options.isWebUI && !options.json && options.logLevel !== "debug") return ora({
		text: "",
		color: "green"
	}).start();
}
/**
* Display scan results in the appropriate format
*
* @param response - Scan response from server
* @param duration - Duration of scan in milliseconds
* @param options - Output options
*/
function displayScanResults(response, duration, options) {
	if (options.json) console.log(JSON.stringify(response, null, 2));
	else {
		const { comments, review } = response;
		const severityCounts = countBySeverity(comments || []);
		printBorder();
		logger_default.info(`${chalk.green("✓")} Scan complete (${formatDuration(duration / 1e3)})`);
		if (severityCounts.total > 0) logger_default.info(chalk.yellow(`⚠ Found ${severityCounts.total} issue${severityCounts.total === 1 ? "" : "s"}`));
		printBorder();
		let reviewText = review;
		if (!reviewText && comments && comments.length > 0) {
			const noneComment = comments.find((c) => c.severity === CodeScanSeverity.NONE);
			if (noneComment) reviewText = noneComment.finding;
		}
		if (reviewText) {
			logger_default.info("");
			logger_default.info(reviewText);
			logger_default.info("");
			printBorder();
		}
		if (severityCounts.total > 0) {
			const validSeverities = [
				CodeScanSeverity.CRITICAL,
				CodeScanSeverity.HIGH,
				CodeScanSeverity.MEDIUM,
				CodeScanSeverity.LOW
			];
			const sortedComments = [...(comments || []).filter((c) => c.severity && validSeverities.includes(c.severity))].sort((a, b) => {
				const rankA = a.severity ? getSeverityRank(a.severity) : 0;
				return (b.severity ? getSeverityRank(b.severity) : 0) - rankA;
			});
			logger_default.info("");
			for (let i = 0; i < sortedComments.length; i++) {
				const comment = sortedComments[i];
				const severity = formatSeverity(comment.severity);
				const location = comment.line ? `${comment.file}:${comment.line}` : comment.file || "";
				logger_default.info(`${severity} ${chalk.gray(location)}`);
				logger_default.info("");
				logger_default.info(comment.finding);
				if (comment.fix) {
					logger_default.info("");
					logger_default.info(chalk.bold("Suggested Fix:"));
					logger_default.info(comment.fix);
				}
				if (comment.aiAgentPrompt) {
					logger_default.info("");
					logger_default.info(chalk.bold("AI Agent Prompt:"));
					logger_default.info(comment.aiAgentPrompt);
				}
				if (i < sortedComments.length - 1) {
					logger_default.info("");
					logger_default.info(chalk.gray("─".repeat(TERMINAL_MAX_WIDTH)));
					logger_default.info("");
				}
			}
			printBorder();
			if (options.githubPr) {
				logger_default.info(`» Comments posted to PR: ${chalk.cyan(options.githubPr)}`);
				printBorder();
			}
		}
	}
}

//#endregion
//#region src/codeScan/scanner/request.ts
/**
* Build scan request from inputs
*
* @param files - Files to scan
* @param metadata - Git metadata
* @param config - Scan configuration
* @param sessionId - Session ID for scan tracking and cancellation
* @param pullRequest - Optional PR context
* @param guidance - Optional custom guidance
* @returns Scan request object
*/
function buildScanRequest(files, metadata, config, sessionId, pullRequest, guidance) {
	return {
		files,
		metadata,
		config: {
			minimumSeverity: config.minimumSeverity,
			diffsOnly: config.diffsOnly,
			guidance
		},
		sessionId,
		pullRequest
	};
}
/**
* Execute scan request via Socket.IO
*
* @param socket - Connected Socket.IO socket
* @param request - Scan request to send
* @param options - Execution options
* @returns Promise resolving to scan response
* @throws Error if scan fails, connection lost, or user cancels
*/
async function executeScanRequest(socket, request, options) {
	const { showSpinner, spinner, abortController } = options;
	if (showSpinner && spinner) spinner.text = "Scanning...";
	let heartbeatInterval;
	let firstPulseTimeout;
	if (showSpinner && spinner) {
		const pulse = () => {
			spinner.text = "Still scanning...";
			setTimeout(() => {
				if (spinner?.isSpinning) spinner.text = "Scanning...";
			}, 4e3);
		};
		firstPulseTimeout = setTimeout(() => {
			pulse();
			heartbeatInterval = setInterval(pulse, 12e3);
		}, 8e3);
	}
	return await new Promise((resolve$1, reject) => {
		const onComplete = (response) => {
			socket?.off("scan:complete", onComplete);
			socket?.off("scan:error", onError);
			socket?.off("reconnect_failed", onReconnectFailed);
			abortController.signal.removeEventListener("abort", onAbort);
			if (firstPulseTimeout) clearTimeout(firstPulseTimeout);
			if (heartbeatInterval) clearInterval(heartbeatInterval);
			resolve$1(response);
		};
		const onError = (error) => {
			socket?.off("scan:complete", onComplete);
			socket?.off("scan:error", onError);
			socket?.off("reconnect_failed", onReconnectFailed);
			abortController.signal.removeEventListener("abort", onAbort);
			if (firstPulseTimeout) clearTimeout(firstPulseTimeout);
			if (heartbeatInterval) clearInterval(heartbeatInterval);
			reject(new Error(error.message || error.error));
		};
		const onReconnectFailed = () => {
			socket?.off("scan:complete", onComplete);
			socket?.off("scan:error", onError);
			socket?.off("reconnect_failed", onReconnectFailed);
			abortController.signal.removeEventListener("abort", onAbort);
			if (firstPulseTimeout) clearTimeout(firstPulseTimeout);
			if (heartbeatInterval) clearInterval(heartbeatInterval);
			reject(/* @__PURE__ */ new Error("Lost connection to server during scan"));
		};
		const onAbort = () => {
			socket?.emit("scan:cancel");
			socket?.off("scan:complete", onComplete);
			socket?.off("scan:error", onError);
			socket?.off("reconnect_failed", onReconnectFailed);
			abortController.signal.removeEventListener("abort", onAbort);
			if (firstPulseTimeout) clearTimeout(firstPulseTimeout);
			if (heartbeatInterval) clearInterval(heartbeatInterval);
			reject(/* @__PURE__ */ new Error("cancelled by user"));
		};
		socket?.on("scan:complete", onComplete);
		socket?.on("scan:error", onError);
		socket?.on("reconnect_failed", onReconnectFailed);
		abortController.signal.addEventListener("abort", onAbort);
		socket?.emit("scan:start", request);
	});
}

//#endregion
//#region src/codeScan/scanner/socket.ts
/**
* Socket.IO Connection Management
*
* Handles creation and configuration of Socket.IO connections for code scanning.
*/
/**
* Create and configure Socket.IO connection
*
* @param apiHost - API host URL to connect to
* @param auth - Authentication credentials
* @returns Promise resolving to connected Socket
* @throws Error if connection fails or times out
*/
async function createSocketConnection(apiHost, auth) {
	return new Promise((resolve$1, reject) => {
		logger_default.debug(`Connecting to ${apiHost}...`);
		let settled = false;
		const cleanup = () => {
			clearTimeout(timeoutId);
			socket.io.reconnection(false);
			socket.removeAllListeners();
			socket.close();
		};
		const socket = io(apiHost, {
			transports: ["websocket"],
			reconnection: true,
			reconnectionDelay: 500,
			reconnectionDelayMax: 1e4,
			reconnectionAttempts: 5,
			auth
		});
		socket.on("connect", () => {
			if (settled) return;
			settled = true;
			clearTimeout(timeoutId);
			logger_default.debug(`Socket.io connected (id: ${socket.id})`);
			resolve$1(socket);
		});
		socket.on("connect_error", (error) => {
			if (settled) return;
			settled = true;
			logger_default.debug(`Socket.io connection error: ${error.message}`);
			cleanup();
			reject(/* @__PURE__ */ new Error(`Failed to connect to server: ${error.message}`));
		});
		socket.on("disconnect", (reason) => {
			logger_default.debug(`Socket.io disconnected: ${reason}`);
		});
		socket.on("error", (error) => {
			logger_default.debug(`Socket.io error: ${String(error)}`);
		});
		const timeoutId = setTimeout(() => {
			if (!socket.connected && !settled) {
				settled = true;
				cleanup();
				reject(/* @__PURE__ */ new Error("Connection timeout after 5 seconds"));
			}
		}, 5e3);
	});
}

//#endregion
//#region src/codeScan/scanner/index.ts
/**
* Core Scan Execution Logic
*
* Main entry point for scanner module - orchestrates the complete scan process.
*/
/**
* Execute a complete security scan
*
* This is the main entry point for the scanner - it orchestrates:
* - Configuration loading
* - Socket.IO connection
* - MCP bridge setup (if not diffs-only)
* - Git diff processing
* - Scan request execution
* - Result display
* - Cleanup
*
* @param repoPath - Path to repository to scan
* @param options - Scan options from CLI
*/
async function executeScan(repoPath, options) {
	let socket = null;
	let mcpProcess = null;
	let mcpBridge = null;
	let sessionId = void 0;
	const startTime = Date.now();
	const config = mergeConfigWithOptions(loadConfigOrDefault(options.config), options);
	const guidance = resolveGuidance(options, config);
	const absoluteRepoPath = path.resolve(repoPath);
	if (!options.json) {
		logger_default.info("Beginning scan for LLM-related vulnerabilities in your code.");
		logger_default.info(`  Minimum severity: ${config.minimumSeverity}`);
		if (config.diffsOnly) logger_default.info(`  Mode: diffs only`);
		else logger_default.info(`  Mode: diffs + tracing into repo`);
		logger_default.info("");
	}
	logger_default.debug(`Repository: ${absoluteRepoPath}`);
	const cleanupRefs = {
		repoPath: absoluteRepoPath,
		socket: null,
		mcpBridge: null,
		mcpProcess: null,
		spinner: null,
		abortController: null
	};
	registerCleanupHandlers(cleanupRefs);
	const isWebUI = Boolean(cliState_default.webUI);
	const spinner = createSpinner({
		json: options.json || false,
		isWebUI,
		logLevel: getLogLevel()
	});
	if (spinner) cleanupRefs.spinner = spinner;
	const showSpinner = Boolean(spinner);
	try {
		const abortController = new AbortController();
		cleanupRefs.abortController = abortController;
		let parsedPR;
		if (options.githubPr) {
			const parsed = parseGitHubPr(options.githubPr);
			if (!parsed) throw new Error(`Invalid --github-pr format: "${options.githubPr}". Expected format: owner/repo#number (e.g., promptfoo/promptfoo#123)`);
			parsedPR = parsed;
		}
		const auth = resolveAuthCredentials(options.apiKey, parsedPR);
		const apiHost = resolveApiHost(options, config);
		logger_default.debug(`Promptfoo API host URL: ${apiHost}`);
		if (!showSpinner) logger_default.debug("Connecting to server...");
		socket = await createSocketConnection(apiHost, auth);
		cleanupRefs.socket = socket;
		sessionId = crypto.randomUUID();
		logger_default.debug(`Session ID: ${sessionId}`);
		socket.emit("scan:session", { sessionId });
		if (!config.diffsOnly) {
			const mcpSetup = await setupMcpBridge(socket, absoluteRepoPath, sessionId);
			mcpProcess = mcpSetup.mcpProcess;
			mcpBridge = mcpSetup.mcpBridge;
			cleanupRefs.mcpProcess = mcpProcess;
			cleanupRefs.mcpBridge = mcpBridge;
		}
		logger_default.debug("Processing git diff...");
		const simpleGit$1 = (await import("simple-git")).default;
		const git = simpleGit$1(absoluteRepoPath);
		if (!options.compare) await validateOnBranch(git);
		let baseBranch;
		if (options.base) baseBranch = options.base;
		else {
			const branches = await git.branch();
			baseBranch = branches.all.includes("main") || branches.all.includes("origin/main") ? "main" : branches.all.includes("master") || branches.all.includes("origin/master") ? "master" : "main";
		}
		const compareRef = options.compare || "HEAD";
		logger_default.debug(`Comparing: ${baseBranch}...${compareRef}`);
		const files = await processDiff(absoluteRepoPath, baseBranch, compareRef);
		const includedFiles = files.filter((f) => !f.skipReason && f.patch);
		const skippedFiles = files.filter((f) => f.skipReason);
		logger_default.debug(`Files changed: ${files.length} (${includedFiles.length} included, ${skippedFiles.length} skipped)`);
		if (includedFiles.length === 0) {
			const msg = "No files to scan";
			if (options.json) {
				const response = {
					success: true,
					comments: [],
					review: msg
				};
				logger_default.info(JSON.stringify(response, null, 2));
			} else if (showSpinner && spinner) spinner.succeed(msg);
			else logger_default.info(msg);
			cliState_default.postActionCallback = async () => {
				await new Promise((resolve$1) => setTimeout(resolve$1, 100));
				process.exitCode = 0;
			};
			return;
		}
		const metadata = await extractMetadata(absoluteRepoPath, baseBranch, compareRef);
		logger_default.debug(`Compare ref: ${metadata.branch}`);
		logger_default.debug(`Commits: ${metadata.commitMessages.length}`);
		let pullRequest = void 0;
		if (parsedPR) {
			const currentCommit = await git.revparse(["HEAD"]);
			pullRequest = {
				owner: parsedPR.owner,
				repo: parsedPR.repo,
				number: parsedPR.number,
				sha: currentCommit.trim()
			};
			logger_default.debug(`GitHub PR context: ${parsedPR.owner}/${parsedPR.repo}#${parsedPR.number} (${pullRequest.sha.substring(0, 7)})`);
		}
		if (!showSpinner) logger_default.debug("Scanning code...");
		const scanRequest = buildScanRequest(files, metadata, config, sessionId, pullRequest, guidance);
		const scanResponse = await executeScanRequest(socket, scanRequest, {
			showSpinner,
			spinner,
			abortController
		});
		if (showSpinner && spinner) spinner.stop();
		displayScanResults(scanResponse, Date.now() - startTime, {
			json: options.json || false,
			githubPr: options.githubPr
		});
	} catch (error) {
		const msg = `Scan failed: ${error instanceof Error ? error.message : String(error)}`;
		if (showSpinner && spinner) spinner.fail(msg);
		else logger_default.error(msg);
		cliState_default.postActionCallback = async () => {
			await new Promise((resolve$1) => setTimeout(resolve$1, 100));
			if (error instanceof Error && error.message === "cancelled by user") process.exitCode = 130;
			else process.exitCode = 1;
		};
	} finally {
		if (mcpBridge) await mcpBridge.disconnect().catch(() => {
			logger_default.debug("MCP bridge cleanup completed");
		});
		if (mcpProcess) await stopFilesystemMcpServer(mcpProcess).catch(() => {
			logger_default.debug("MCP server cleanup completed");
		});
		if (socket) {
			socket.disconnect();
			logger_default.debug("Socket disconnected");
		}
	}
}

//#endregion
export { executeScan };
//# sourceMappingURL=scanner-BUmYt6J2.js.map