securesync
Version:
Intelligent dependency security scanner with auto-fix
1 lines • 77.4 kB
Source Map (JSON)
{"version":3,"sources":["../src/scanner/npm.ts","../src/analyzer/semver.ts","../src/analyzer/changelog.ts","../src/analyzer/api-diff.ts","../src/analyzer/index.ts","../src/remediation/migrator.ts","../src/remediation/tester.ts","../src/alternatives/scorer.ts","../src/alternatives/finder.ts","../src/graph/builder.ts","../src/graph/visualizer.ts","../src/api/SecureSync.ts"],"sourcesContent":["import { readFile } from 'fs/promises';\nimport { join } from 'path';\nimport { existsSync } from 'fs';\nimport type {\n ScanResult,\n ScanOptions,\n Vulnerability,\n DependencyTree,\n DependencyNode,\n PackageInfo,\n} from './types.js';\n\nexport async function scanNpmProject(projectPath: string, options: Partial<ScanOptions> = {}): Promise<ScanResult> {\n // 1. Read package.json and package-lock.json\n const packageJson = await readPackageJson(projectPath);\n const lockfile = await readLockfile(projectPath);\n\n // 2. Build dependency tree (including transitive deps)\n const depTree = buildDependencyTree(packageJson, lockfile, options.includeDevDependencies ?? false);\n\n // 3. Query npm audit API for vulnerabilities\n const auditResult = await queryNpmAudit(projectPath, depTree);\n\n // 4. Enhance with OSV and NVD data (if enabled)\n const enhanced = options.enhanceWithOSV\n ? await enhanceVulnerabilities(auditResult)\n : auditResult;\n\n // 5. Calculate reachability (are vulnerable packages used?)\n const vulnerabilities = options.analyzeReachability\n ? await analyzeReachability(enhanced, projectPath)\n : enhanced;\n\n // 6. Build summary\n const summary = {\n critical: vulnerabilities.filter(v => v.severity === 'critical').length,\n high: vulnerabilities.filter(v => v.severity === 'high').length,\n moderate: vulnerabilities.filter(v => v.severity === 'moderate').length,\n low: vulnerabilities.filter(v => v.severity === 'low').length,\n };\n\n return {\n vulnerabilities,\n totalPackages: depTree.packages.length,\n scannedAt: new Date(),\n dependencies: depTree,\n summary,\n };\n}\n\nasync function readPackageJson(projectPath: string): Promise<any> {\n const packageJsonPath = join(projectPath, 'package.json');\n\n if (!existsSync(packageJsonPath)) {\n throw new Error(`package.json not found at ${packageJsonPath}`);\n }\n\n const content = await readFile(packageJsonPath, 'utf-8');\n return JSON.parse(content);\n}\n\nasync function readLockfile(projectPath: string): Promise<any> {\n const lockfilePath = join(projectPath, 'package-lock.json');\n\n if (!existsSync(lockfilePath)) {\n throw new Error(`package-lock.json not found at ${lockfilePath}. Please run 'npm install' first.`);\n }\n\n const content = await readFile(lockfilePath, 'utf-8');\n return JSON.parse(content);\n}\n\nfunction buildDependencyTree(\n packageJson: any,\n lockfile: any,\n includeDevDeps: boolean\n): DependencyTree {\n const packages: PackageInfo[] = [];\n const dependencies = new Map<string, DependencyNode>();\n\n // Process direct dependencies\n const deps = packageJson.dependencies || {};\n const devDeps = includeDevDeps ? (packageJson.devDependencies || {}) : {};\n\n for (const [name] of Object.entries(deps)) {\n const lockEntry = lockfile.packages?.[`node_modules/${name}`];\n if (lockEntry) {\n dependencies.set(name, {\n name,\n version: lockEntry.version,\n resolved: lockEntry.resolved || '',\n dependencies: buildTransitiveDeps(lockfile, lockEntry),\n });\n packages.push({\n name,\n version: lockEntry.version,\n isDevDependency: false,\n isDirect: true,\n });\n }\n }\n\n for (const [name] of Object.entries(devDeps)) {\n const lockEntry = lockfile.packages?.[`node_modules/${name}`];\n if (lockEntry) {\n dependencies.set(name, {\n name,\n version: lockEntry.version,\n resolved: lockEntry.resolved || '',\n dependencies: buildTransitiveDeps(lockfile, lockEntry),\n });\n packages.push({\n name,\n version: lockEntry.version,\n isDevDependency: true,\n isDirect: true,\n });\n }\n }\n\n // Collect all packages (including transitive)\n collectAllPackages(dependencies, packages);\n\n return {\n name: packageJson.name,\n version: packageJson.version,\n dependencies,\n packages,\n };\n}\n\nfunction buildTransitiveDeps(lockfile: any, lockEntry: any): Map<string, DependencyNode> | undefined {\n if (!lockEntry.dependencies) {\n return undefined;\n }\n\n const transitive = new Map<string, DependencyNode>();\n for (const [name] of Object.entries(lockEntry.dependencies)) {\n const transitiveEntry = lockfile.packages?.[`node_modules/${name}`];\n if (transitiveEntry) {\n transitive.set(name, {\n name,\n version: transitiveEntry.version,\n resolved: transitiveEntry.resolved || '',\n dependencies: buildTransitiveDeps(lockfile, transitiveEntry),\n });\n }\n }\n\n return transitive.size > 0 ? transitive : undefined;\n}\n\nfunction collectAllPackages(deps: Map<string, DependencyNode>, packages: PackageInfo[]): void {\n const seen = new Set<string>(packages.map(p => `${p.name}@${p.version}`));\n\n function traverse(depMap: Map<string, DependencyNode> | undefined, isDev: boolean): void {\n if (!depMap) return;\n\n for (const [name, node] of depMap) {\n const key = `${name}@${node.version}`;\n if (!seen.has(key)) {\n seen.add(key);\n packages.push({\n name,\n version: node.version,\n isDevDependency: isDev,\n isDirect: false,\n });\n }\n traverse(node.dependencies, isDev);\n }\n }\n\n for (const [, node] of deps) {\n traverse(node.dependencies, false);\n }\n}\n\nasync function queryNpmAudit(_projectPath: string, _depTree: DependencyTree): Promise<Vulnerability[]> {\n // This would normally call the npm audit API\n // For now, returning a mock implementation structure\n const vulnerabilities: Vulnerability[] = [];\n\n try {\n // In a real implementation, this would:\n // 1. Use npm-registry-fetch to query the audit endpoint\n // 2. Parse the audit report\n // 3. Map vulnerabilities to our format\n\n // Placeholder for actual npm audit API call\n // const auditData = await npmFetch.json('/-/npm/v1/security/audits', {\n // method: 'POST',\n // body: JSON.stringify({ /* lockfile data */ })\n // });\n\n return vulnerabilities;\n } catch (error) {\n console.error('Error querying npm audit:', error);\n return vulnerabilities;\n }\n}\n\nasync function enhanceVulnerabilities(vulnerabilities: Vulnerability[]): Promise<Vulnerability[]> {\n // This would enhance with OSV and NVD data\n // For now, return as-is\n return vulnerabilities;\n}\n\nasync function analyzeReachability(\n vulnerabilities: Vulnerability[],\n _projectPath: string\n): Promise<Vulnerability[]> {\n // This would analyze if vulnerable code is actually imported/used\n // For now, return all vulnerabilities\n return vulnerabilities;\n}\n","import { compare, diff, valid, type ReleaseType } from 'semver';\n\nexport interface VersionDiff {\n fromVersion: string;\n toVersion: string;\n diffType: ReleaseType | null;\n isUpgrade: boolean;\n isDowngrade: boolean;\n expectedBreakingChanges: boolean;\n}\n\nexport function analyzeVersionDiff(fromVersion: string, toVersion: string): VersionDiff {\n if (!valid(fromVersion) || !valid(toVersion)) {\n throw new Error(`Invalid semver versions: ${fromVersion} or ${toVersion}`);\n }\n\n const compareResult = compare(fromVersion, toVersion);\n const diffType = diff(fromVersion, toVersion);\n\n return {\n fromVersion,\n toVersion,\n diffType,\n isUpgrade: compareResult < 0,\n isDowngrade: compareResult > 0,\n expectedBreakingChanges: diffType === 'major' || diffType === 'premajor',\n };\n}\n\nexport function shouldAnalyzeBreakingChanges(versionDiff: VersionDiff): boolean {\n // Always analyze if it's a major version bump\n if (versionDiff.expectedBreakingChanges) {\n return true;\n }\n\n // Also analyze minor and patch updates as they might have undeclared breaking changes\n return versionDiff.isUpgrade && versionDiff.diffType !== null;\n}\n","import type { ChangelogEntry } from './types.js';\n\nexport async function parseChangelog(changelogContent: string): Promise<ChangelogEntry[]> {\n const entries: ChangelogEntry[] = [];\n\n // Split by version headers (common formats: ## [1.0.0], # 1.0.0, etc.)\n const versionRegex = /^##?\\s*\\[?(\\d+\\.\\d+\\.\\d+[^\\]]*)\\]?.*$/gm;\n const matches = [...changelogContent.matchAll(versionRegex)];\n\n for (let i = 0; i < matches.length; i++) {\n const match = matches[i];\n const version = match[1];\n const startIndex = match.index! + match[0].length;\n const endIndex = i < matches.length - 1 ? matches[i + 1].index! : changelogContent.length;\n const content = changelogContent.slice(startIndex, endIndex);\n\n entries.push({\n version,\n date: extractDate(match[0]),\n changes: categorizeChanges(content),\n });\n }\n\n return entries;\n}\n\nfunction extractDate(headerLine: string): string | undefined {\n const dateRegex = /\\d{4}-\\d{2}-\\d{2}/;\n const match = headerLine.match(dateRegex);\n return match ? match[0] : undefined;\n}\n\nfunction categorizeChanges(content: string): ChangelogEntry['changes'] {\n const changes = {\n breaking: [] as string[],\n features: [] as string[],\n fixes: [] as string[],\n other: [] as string[],\n };\n\n const lines = content.split('\\n');\n let currentCategory: keyof typeof changes = 'other';\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Detect category headers\n if (/^###?\\s*(breaking|breaking changes)/i.test(trimmed)) {\n currentCategory = 'breaking';\n continue;\n } else if (/^###?\\s*(features?|added)/i.test(trimmed)) {\n currentCategory = 'features';\n continue;\n } else if (/^###?\\s*(fix(es)?|bug\\s*fix(es)?)/i.test(trimmed)) {\n currentCategory = 'fixes';\n continue;\n }\n\n // Extract bullet points\n if (/^[-*]\\s/.test(trimmed)) {\n const change = trimmed.replace(/^[-*]\\s/, '').trim();\n if (change) {\n changes[currentCategory].push(change);\n }\n }\n }\n\n return changes;\n}\n\nexport function extractMigrations(changelog: ChangelogEntry[], targetVersion: string): Map<string, string> {\n const migrations = new Map<string, string>();\n\n const entry = changelog.find(e => e.version === targetVersion);\n if (!entry) {\n return migrations;\n }\n\n // Look for migration hints in breaking changes\n for (const change of entry.changes.breaking) {\n const migrationHint = extractMigrationHint(change);\n if (migrationHint) {\n migrations.set(migrationHint.from, migrationHint.to);\n }\n }\n\n return migrations;\n}\n\nfunction extractMigrationHint(change: string): { from: string; to: string } | null {\n // Look for patterns like \"replace X with Y\" or \"use Y instead of X\"\n const replacePattern = /replace\\s+`?(\\w+)`?\\s+with\\s+`?(\\w+)`?/i;\n const insteadPattern = /use\\s+`?(\\w+)`?\\s+instead\\s+of\\s+`?(\\w+)`?/i;\n\n let match = change.match(replacePattern);\n if (match) {\n return { from: match[1], to: match[2] };\n }\n\n match = change.match(insteadPattern);\n if (match) {\n return { from: match[2], to: match[1] };\n }\n\n return null;\n}\n","import type { APIChange } from './types.js';\n\nexport interface TypeDefinition {\n kind: 'function' | 'class' | 'interface' | 'type' | 'variable';\n name: string;\n signature: string;\n exported: boolean;\n}\n\nexport function diffAPIs(oldTypes: TypeDefinition[], newTypes: TypeDefinition[]): APIChange[] {\n const changes: APIChange[] = [];\n\n // Create maps for efficient lookup\n const oldMap = new Map(oldTypes.map(t => [t.name, t]));\n const newMap = new Map(newTypes.map(t => [t.name, t]));\n\n // Find removed or modified APIs\n for (const [name, oldType] of oldMap) {\n if (!oldType.exported) continue;\n\n const newType = newMap.get(name);\n\n if (!newType) {\n // API was removed\n changes.push({\n type: 'breaking',\n category: 'removed',\n symbol: name,\n before: oldType.signature,\n after: '',\n confidence: 1.0,\n source: 'typescript',\n });\n } else if (newType.signature !== oldType.signature) {\n // API signature changed\n changes.push({\n type: 'breaking',\n category: 'signature',\n symbol: name,\n before: oldType.signature,\n after: newType.signature,\n confidence: 0.9,\n source: 'typescript',\n });\n }\n }\n\n // Find added APIs (features, not breaking)\n for (const [name, newType] of newMap) {\n if (!newType.exported) continue;\n\n if (!oldMap.has(name)) {\n changes.push({\n type: 'feature',\n category: 'signature',\n symbol: name,\n before: '',\n after: newType.signature,\n confidence: 1.0,\n source: 'typescript',\n });\n }\n }\n\n return changes;\n}\n\nexport async function parseTypeDefinitions(packagePath: string): Promise<TypeDefinition[]> {\n // This would parse .d.ts files using TypeScript compiler API\n // For now, return empty array as placeholder\n const definitions: TypeDefinition[] = [];\n\n // In a real implementation:\n // 1. Find .d.ts files in the package\n // 2. Parse them using TypeScript compiler API\n // 3. Extract exported symbols and their signatures\n // 4. Return as TypeDefinition[]\n\n return definitions;\n}\n\nexport function classifyChanges(\n apiDiff: APIChange[],\n migrations: Map<string, string>\n): APIChange[] {\n return apiDiff.map(change => {\n // Add migration hints from changelog\n if (change.category === 'removed' || change.category === 'renamed') {\n const migration = migrations.get(change.symbol);\n if (migration) {\n return {\n ...change,\n migration: `Replace \\`${change.symbol}\\` with \\`${migration}\\``,\n confidence: Math.min(change.confidence + 0.1, 1.0),\n };\n }\n }\n\n return change;\n });\n}\n","import { analyzeVersionDiff, shouldAnalyzeBreakingChanges } from './semver.js';\nimport { parseChangelog, extractMigrations } from './changelog.js';\nimport { diffAPIs, parseTypeDefinitions, classifyChanges } from './api-diff.js';\nimport type { BreakingChangeAnalysis, APIChange } from './types.js';\n\nexport async function analyzeBreakingChanges(\n packageName: string,\n fromVersion: string,\n toVersion: string\n): Promise<BreakingChangeAnalysis> {\n // 1. Compare semantic versions\n const versionDiff = analyzeVersionDiff(fromVersion, toVersion);\n\n if (!shouldAnalyzeBreakingChanges(versionDiff)) {\n return {\n packageName,\n fromVersion,\n toVersion,\n changes: [],\n hasBreakingChanges: false,\n riskLevel: 'low',\n analyzedAt: new Date(),\n };\n }\n\n // 2. Download and analyze both package versions\n const oldPackagePath = await downloadPackage(packageName, fromVersion);\n const newPackagePath = await downloadPackage(packageName, toVersion);\n\n // 3. Parse TypeScript definitions if available\n const oldTypes = await parseTypeDefinitions(oldPackagePath);\n const newTypes = await parseTypeDefinitions(newPackagePath);\n\n // 4. Diff the public API surface\n const apiDiff = diffAPIs(oldTypes, newTypes);\n\n // 5. Parse CHANGELOG.md for migration hints\n const changelog = await loadChangelog(newPackagePath);\n const changelogEntries = changelog ? await parseChangelog(changelog) : [];\n const migrations = extractMigrations(changelogEntries, toVersion);\n\n // 6. Classify changes and add migration hints\n const changes = classifyChanges(apiDiff, migrations);\n\n // Add breaking changes from changelog that weren't detected in API diff\n const changelogBreaking = changelogEntries.find(e => e.version === toVersion);\n if (changelogBreaking) {\n for (const breaking of changelogBreaking.changes.breaking) {\n // Only add if not already detected\n if (!changes.some(c => c.symbol === breaking)) {\n changes.push({\n type: 'breaking',\n category: 'behavior',\n symbol: breaking,\n before: '',\n after: '',\n confidence: 0.7,\n source: 'changelog',\n });\n }\n }\n }\n\n // 7. Calculate risk level\n const riskLevel = calculateRiskLevel(changes, versionDiff.expectedBreakingChanges);\n\n return {\n packageName,\n fromVersion,\n toVersion,\n changes,\n hasBreakingChanges: changes.some(c => c.type === 'breaking'),\n riskLevel,\n analyzedAt: new Date(),\n };\n}\n\nasync function downloadPackage(packageName: string, version: string): Promise<string> {\n // This would use pacote to download the package\n // For now, return a placeholder path\n return `/tmp/securesync-cache/${packageName}@${version}`;\n}\n\nasync function loadChangelog(_packagePath: string): Promise<string | null> {\n // This would read CHANGELOG.md, HISTORY.md, or similar files\n // For now, return null\n return null;\n}\n\nfunction calculateRiskLevel(\n changes: APIChange[],\n expectedBreaking: boolean\n): 'low' | 'medium' | 'high' {\n const breakingChanges = changes.filter(c => c.type === 'breaking');\n\n if (breakingChanges.length === 0) {\n return 'low';\n }\n\n if (breakingChanges.length > 5 || !expectedBreaking) {\n return 'high';\n }\n\n return 'medium';\n}\n\nexport type { APIChange, BreakingChangeAnalysis, ChangelogEntry } from './types.js';\nexport { analyzeVersionDiff } from './semver.js';\nexport { parseChangelog } from './changelog.js';\n","import { readFile } from 'fs/promises';\nimport { parse } from '@babel/parser';\nimport traverse from '@babel/traverse';\nimport generate from '@babel/generator';\nimport type { APIChange } from '../analyzer/types.js';\nimport type { Migration, CodeChange } from './types.js';\n\nexport async function generateMigration(\n projectPath: string,\n packageName: string,\n changes: APIChange[]\n): Promise<Migration[]> {\n const migrations: Migration[] = [];\n\n // 1. Find all files importing the updated package\n const affectedFiles = await findImports(projectPath, packageName);\n\n for (const file of affectedFiles) {\n // 2. Parse file into AST\n const code = await readFile(file, 'utf-8');\n let ast;\n\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n });\n } catch (error) {\n console.warn(`Failed to parse ${file}:`, error);\n continue;\n }\n\n // 3. Find usages of changed APIs\n const usages: CodeChange[] = [];\n const breakingChanges = changes.filter(c => c.type === 'breaking');\n\n traverse(ast, {\n CallExpression(path) {\n const callee = path.node.callee;\n const affectedChange = findAffectedAPI(callee, breakingChanges);\n\n if (affectedChange) {\n const generated = generate(path.node);\n const replacement = generateReplacement(path.node, affectedChange);\n\n if (replacement) {\n usages.push({\n line: path.node.loc?.start.line || 0,\n column: path.node.loc?.start.column || 0,\n old: generated.code,\n new: replacement,\n });\n }\n }\n },\n ImportDeclaration(path) {\n if (path.node.source.value === packageName) {\n // Check if any imported names have changed\n for (const specifier of path.node.specifiers) {\n if (specifier.type === 'ImportSpecifier') {\n const importedName = specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : '';\n\n const change = breakingChanges.find(\n c => c.symbol === importedName && c.category === 'renamed'\n );\n\n if (change && change.migration) {\n const generated = generate(specifier);\n usages.push({\n line: specifier.loc?.start.line || 0,\n column: specifier.loc?.start.column || 0,\n old: generated.code,\n new: change.migration,\n });\n }\n }\n }\n }\n },\n });\n\n // 4. Generate migration script\n if (usages.length > 0) {\n migrations.push({\n file,\n changes: usages,\n script: generateMigrationScript(file, usages),\n safe: determineSafety(usages, breakingChanges),\n });\n }\n }\n\n return migrations;\n}\n\nasync function findImports(_projectPath: string, _packageName: string): Promise<string[]> {\n // This would use a tool like grep or a custom file walker\n // to find all files importing the package\n // For now, return empty array\n return [];\n}\n\nfunction findAffectedAPI(callee: any, changes: APIChange[]): APIChange | null {\n if (callee.type === 'Identifier') {\n return changes.find(c => c.symbol === callee.name) || null;\n }\n\n if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {\n return changes.find(c => c.symbol === callee.property.name) || null;\n }\n\n return null;\n}\n\nfunction generateReplacement(_node: any, change: APIChange): string | null {\n if (change.migration) {\n // If there's an explicit migration hint, use it\n return change.migration;\n }\n\n // Try to infer a replacement based on the change type\n if (change.category === 'signature') {\n // Signature changed but we don't have a migration hint\n // Return null to indicate manual intervention needed\n return null;\n }\n\n return null;\n}\n\nfunction generateMigrationScript(file: string, changes: CodeChange[]): string {\n let script = `// Migration script for ${file}\\n\\n`;\n\n script += `import { readFile, writeFile } from 'fs/promises';\\n\\n`;\n script += `async function migrate() {\\n`;\n script += ` const content = await readFile('${file}', 'utf-8');\\n`;\n script += ` let updated = content;\\n\\n`;\n\n for (const change of changes) {\n script += ` // Line ${change.line}: ${change.old} -> ${change.new}\\n`;\n script += ` updated = updated.replace(\\n`;\n script += ` ${JSON.stringify(change.old)},\\n`;\n script += ` ${JSON.stringify(change.new)}\\n`;\n script += ` );\\n\\n`;\n }\n\n script += ` await writeFile('${file}', updated, 'utf-8');\\n`;\n script += ` console.log('Migration completed for ${file}');\\n`;\n script += `}\\n\\n`;\n script += `migrate().catch(console.error);\\n`;\n\n return script;\n}\n\nfunction determineSafety(changes: CodeChange[], apiChanges: APIChange[]): boolean {\n // Consider it safe if:\n // 1. All changes have high confidence (>0.8)\n // 2. All changes have migration hints\n // 3. Number of changes is small (<= 5)\n\n if (changes.length > 5) {\n return false;\n }\n\n const hasLowConfidence = apiChanges.some(c => c.confidence < 0.8);\n if (hasLowConfidence) {\n return false;\n }\n\n const hasMissingMigrations = changes.some(c => !c.new || c.new === c.old);\n if (hasMissingMigrations) {\n return false;\n }\n\n return true;\n}\n","import { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { readFile } from 'fs/promises';\nimport { join } from 'path';\nimport type { TestResult, UpdateResult, RemediationOptions } from './types.js';\nimport type { Migration } from './types.js';\n\nconst execAsync = promisify(exec);\n\nexport async function testDrivenUpdate(\n projectPath: string,\n packageName: string,\n newVersion: string,\n migrations: Migration[],\n options: RemediationOptions = {}\n): Promise<UpdateResult> {\n const shouldRunTests = options.runTests ?? true;\n const shouldCreateBackup = options.createBackup ?? true;\n\n // 1. Run tests before update (baseline)\n if (shouldRunTests) {\n const baselineTests = await runTests(projectPath);\n if (!baselineTests.passed) {\n return {\n success: false,\n reason: 'Tests failing before update - please fix existing test failures first',\n failedTests: baselineTests.failedTests,\n };\n }\n }\n\n // 2. Create backup of package.json and lock file\n if (shouldCreateBackup) {\n await createBackupFiles(projectPath);\n }\n\n try {\n // 3. Update package to new version\n await updatePackage(projectPath, packageName, newVersion);\n\n // 4. Apply migration scripts\n if (options.autoApply && migrations.length > 0) {\n for (const migration of migrations) {\n if (migration.safe || options.interactive === false) {\n await applyMigration(migration);\n }\n }\n }\n\n // 5. Run tests after update\n if (shouldRunTests) {\n const updatedTests = await runTests(projectPath);\n\n if (!updatedTests.passed) {\n // 6. Tests failed - rollback everything\n await rollback(projectPath);\n return {\n success: false,\n reason: 'Tests failed after update',\n failedTests: updatedTests.failedTests,\n migrations,\n rolledBack: true,\n };\n }\n }\n\n // 7. Tests passed - commit changes\n return {\n success: true,\n migrations,\n };\n } catch (error) {\n if (shouldCreateBackup) {\n await rollback(projectPath);\n }\n throw error;\n }\n}\n\nexport async function runTests(projectPath: string): Promise<TestResult> {\n const startTime = Date.now();\n\n try {\n // Detect test command from package.json\n const testCommand = await detectTestCommand(projectPath);\n\n if (!testCommand) {\n return {\n passed: true,\n output: 'No test command found, skipping tests',\n duration: Date.now() - startTime,\n };\n }\n\n const { stdout } = await execAsync(testCommand, {\n cwd: projectPath,\n env: { ...process.env, CI: 'true' },\n });\n\n return {\n passed: true,\n output: stdout,\n duration: Date.now() - startTime,\n exitCode: 0,\n };\n } catch (error: any) {\n return {\n passed: false,\n output: error.stderr || error.stdout || error.message,\n duration: Date.now() - startTime,\n failedTests: parseFailedTests(error.stderr || error.stdout || ''),\n exitCode: error.code,\n };\n }\n}\n\nasync function detectTestCommand(projectPath: string): Promise<string | null> {\n try {\n const packageJsonPath = join(projectPath, 'package.json');\n const content = await readFile(packageJsonPath, 'utf-8');\n const packageJson = JSON.parse(content);\n\n return packageJson.scripts?.test || null;\n } catch {\n return null;\n }\n}\n\nfunction parseFailedTests(output: string): string[] {\n const failed: string[] = [];\n\n // Common test failure patterns\n const patterns = [\n /FAIL\\s+(.+)/g,\n /✖\\s+(.+)/g,\n /×\\s+(.+)/g,\n /ERROR\\s+(.+)/g,\n ];\n\n for (const pattern of patterns) {\n const matches = output.matchAll(pattern);\n for (const match of matches) {\n if (match[1] && !failed.includes(match[1])) {\n failed.push(match[1].trim());\n }\n }\n }\n\n return failed;\n}\n\nasync function createBackupFiles(projectPath: string): Promise<void> {\n const { copyFile } = await import('fs/promises');\n\n try {\n await copyFile(\n join(projectPath, 'package.json'),\n join(projectPath, 'package.json.backup')\n );\n\n await copyFile(\n join(projectPath, 'package-lock.json'),\n join(projectPath, 'package-lock.json.backup')\n );\n } catch (error) {\n console.warn('Failed to create backup files:', error);\n }\n}\n\nasync function updatePackage(\n projectPath: string,\n packageName: string,\n version: string\n): Promise<void> {\n await execAsync(`npm install ${packageName}@${version}`, {\n cwd: projectPath,\n });\n}\n\nasync function applyMigration(migration: Migration): Promise<void> {\n const { readFile, writeFile } = await import('fs/promises');\n\n try {\n let content = await readFile(migration.file, 'utf-8');\n\n // Apply each change\n for (const change of migration.changes) {\n content = content.replace(change.old, change.new);\n }\n\n await writeFile(migration.file, content, 'utf-8');\n } catch (error) {\n console.error(`Failed to apply migration to ${migration.file}:`, error);\n throw error;\n }\n}\n\nasync function rollback(projectPath: string): Promise<void> {\n const { copyFile, unlink } = await import('fs/promises');\n\n try {\n // Restore from backup\n await copyFile(\n join(projectPath, 'package.json.backup'),\n join(projectPath, 'package.json')\n );\n\n await copyFile(\n join(projectPath, 'package-lock.json.backup'),\n join(projectPath, 'package-lock.json')\n );\n\n // Reinstall original dependencies\n await execAsync('npm install', { cwd: projectPath });\n\n // Clean up backup files\n await unlink(join(projectPath, 'package.json.backup'));\n await unlink(join(projectPath, 'package-lock.json.backup'));\n } catch (error) {\n console.error('Failed to rollback changes:', error);\n throw error;\n }\n}\n","import type { PackageMetadata } from './types.js';\n\nexport async function scoreAlternative(\n alternative: PackageMetadata,\n _originalPackage: string\n): Promise<number> {\n let score = 0;\n\n // Popularity (30%)\n const popularityScore = calculatePopularityScore(alternative.downloads);\n score += popularityScore * 0.3;\n\n // Maintenance (25%)\n const maintenanceScore = calculateMaintenanceScore(alternative.lastPublish);\n score += maintenanceScore * 0.25;\n\n // Security (25%)\n const securityScore = await calculateSecurityScore(alternative.name);\n score += securityScore * 0.25;\n\n // Quality indicators (20%)\n const qualityScore = await calculateQualityScore(alternative);\n score += qualityScore * 0.2;\n\n return Math.min(100, Math.round(score));\n}\n\nfunction calculatePopularityScore(downloads: number): number {\n // Scale: 0-100 based on weekly downloads\n // 1M+ downloads = 100\n // 100k downloads = 75\n // 10k downloads = 50\n // 1k downloads = 25\n // <1k downloads = 0-25 scaled\n\n if (downloads >= 1000000) {\n return 100;\n } else if (downloads >= 100000) {\n return 75 + ((downloads - 100000) / 900000) * 25;\n } else if (downloads >= 10000) {\n return 50 + ((downloads - 10000) / 90000) * 25;\n } else if (downloads >= 1000) {\n return 25 + ((downloads - 1000) / 9000) * 25;\n } else {\n return (downloads / 1000) * 25;\n }\n}\n\nfunction calculateMaintenanceScore(lastPublish: Date): number {\n const daysSinceUpdate = daysSince(lastPublish);\n\n // Scale: 0-100 based on recency\n // < 30 days = 100\n // < 90 days = 80\n // < 180 days = 60\n // < 365 days = 40\n // < 730 days = 20\n // > 730 days = 0\n\n if (daysSinceUpdate < 30) {\n return 100;\n } else if (daysSinceUpdate < 90) {\n return 80 + ((90 - daysSinceUpdate) / 60) * 20;\n } else if (daysSinceUpdate < 180) {\n return 60 + ((180 - daysSinceUpdate) / 90) * 20;\n } else if (daysSinceUpdate < 365) {\n return 40 + ((365 - daysSinceUpdate) / 185) * 20;\n } else if (daysSinceUpdate < 730) {\n return 20 + ((730 - daysSinceUpdate) / 365) * 20;\n } else {\n return Math.max(0, 20 - ((daysSinceUpdate - 730) / 365) * 20);\n }\n}\n\nasync function calculateSecurityScore(_packageName: string): Promise<number> {\n // This would query vulnerability databases\n // For now, return a placeholder\n // In real implementation:\n // 1. Query npm audit for this package\n // 2. Query OSV database\n // 3. Check for known security advisories\n // No vulnerabilities = 100\n // 1 low/moderate = 80\n // 1+ high/critical = 0\n\n return 100; // Placeholder\n}\n\nasync function calculateQualityScore(pkg: PackageMetadata): Promise<number> {\n let score = 0;\n\n // Has description\n if (pkg.description && pkg.description.length > 20) {\n score += 20;\n }\n\n // Has repository\n if (pkg.repository) {\n score += 20;\n }\n\n // Has homepage\n if (pkg.homepage) {\n score += 15;\n }\n\n // Has keywords\n if (pkg.keywords && pkg.keywords.length >= 3) {\n score += 15;\n }\n\n // Has license\n if (pkg.license) {\n score += 15;\n }\n\n // TypeScript support (would check for .d.ts files or @types package)\n // score += 15;\n\n return score;\n}\n\nfunction daysSince(date: Date): number {\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n return Math.floor(diff / (1000 * 60 * 60 * 24));\n}\n","import type { Alternative, SearchCriteria, PackageMetadata } from './types.js';\nimport { scoreAlternative } from './scorer.js';\n\nexport async function findAlternatives(\n packageName: string,\n criteria: SearchCriteria = {}\n): Promise<Alternative[]> {\n // 1. Query npm for similar packages\n const keywords = await extractKeywords(packageName);\n const similar = await searchNpm({\n keywords,\n exclude: [packageName],\n });\n\n // 2. Score each alternative\n const scored: Alternative[] = await Promise.all(\n similar.map(async (pkg): Promise<Alternative> => {\n const score = await scoreAlternative(pkg, packageName);\n const compatibility = await analyzeAPICompatibility(pkg, packageName);\n const migrationEffort = determineMigrationEffort(compatibility, score);\n\n return {\n name: pkg.name,\n description: pkg.description,\n downloads: pkg.downloads,\n lastPublish: pkg.lastPublish,\n stars: 0, // TODO: fetch from GitHub API\n issues: 0, // TODO: fetch from GitHub API\n maintainers: 0, // TODO: fetch from npm registry\n vulnerabilities: 0, // TODO: fetch from security databases\n compatibility,\n migrationEffort,\n score,\n };\n })\n );\n\n // 3. Filter and sort by score\n return scored\n .filter(alt => meetsCriteria(alt, criteria))\n .sort((a, b) => b.score - a.score)\n .slice(0, 10);\n}\n\nasync function extractKeywords(packageName: string): Promise<string[]> {\n try {\n const metadata = await fetchPackageMetadata(packageName);\n return metadata.keywords || [];\n } catch {\n // If we can't fetch metadata, derive keywords from package name\n return packageName.split('-').filter(k => k.length > 2);\n }\n}\n\nasync function searchNpm(_options: {\n keywords: string[];\n exclude: string[];\n}): Promise<PackageMetadata[]> {\n // This would use npm-registry-fetch to search for packages\n // For now, return empty array as placeholder\n const results: PackageMetadata[] = [];\n\n // In a real implementation:\n // 1. Build search query from keywords\n // 2. Query npm registry search API\n // 3. Parse and normalize results\n // 4. Fetch additional metadata for each result\n // 5. Filter out excluded packages\n\n return results;\n}\n\nasync function fetchPackageMetadata(packageName: string): Promise<PackageMetadata> {\n // This would use npm-registry-fetch to get package metadata\n // For now, return placeholder data\n return {\n name: packageName,\n description: '',\n version: '1.0.0',\n downloads: 0,\n lastPublish: new Date(),\n keywords: [],\n };\n}\n\nasync function analyzeAPICompatibility(\n _alternative: PackageMetadata,\n _original: string\n): Promise<number> {\n // This would analyze how similar the APIs are\n // Could use:\n // 1. TypeScript definition comparison\n // 2. README/documentation similarity\n // 3. Common function names\n // 4. Similar exports\n\n // For now, return a placeholder value\n return 50; // 0-100 scale\n}\n\nfunction determineMigrationEffort(\n compatibility: number,\n _score: number\n): 'low' | 'medium' | 'high' {\n if (compatibility >= 80) {\n return 'low';\n } else if (compatibility >= 50) {\n return 'medium';\n } else {\n return 'high';\n }\n}\n\nfunction meetsCriteria(alternative: Alternative, criteria: SearchCriteria): boolean {\n if (criteria.minDownloads && alternative.downloads < criteria.minDownloads) {\n return false;\n }\n\n if (criteria.maxAge) {\n const daysSincePublish = daysSince(alternative.lastPublish);\n if (daysSincePublish > criteria.maxAge) {\n return false;\n }\n }\n\n if (criteria.minStars && alternative.stars < criteria.minStars) {\n return false;\n }\n\n if (criteria.zeroVulnerabilities && alternative.vulnerabilities > 0) {\n return false;\n }\n\n if (criteria.minCompatibility && alternative.compatibility < criteria.minCompatibility) {\n return false;\n }\n\n return true;\n}\n\nfunction daysSince(date: Date): number {\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n return Math.floor(diff / (1000 * 60 * 60 * 24));\n}\n","import type { DependencyTree, DependencyNode } from '../scanner/types.js';\n\nexport interface GraphNode {\n id: string;\n name: string;\n version: string;\n depth: number;\n parent?: string;\n children: string[];\n vulnerabilities: number;\n isDevDependency: boolean;\n}\n\nexport interface DependencyGraph {\n nodes: Map<string, GraphNode>;\n edges: Array<{ from: string; to: string }>;\n roots: string[];\n}\n\nexport function buildGraph(depTree: DependencyTree): DependencyGraph {\n const nodes = new Map<string, GraphNode>();\n const edges: Array<{ from: string; to: string }> = [];\n const roots: string[] = [];\n\n // Build nodes from dependency tree\n for (const [name, dep] of depTree.dependencies) {\n const nodeId = `${name}@${dep.version}`;\n roots.push(nodeId);\n\n buildNodeRecursive(dep, nodeId, 0, undefined, nodes, edges, false);\n }\n\n return { nodes, edges, roots };\n}\n\nfunction buildNodeRecursive(\n dep: DependencyNode,\n nodeId: string,\n depth: number,\n parent: string | undefined,\n nodes: Map<string, GraphNode>,\n edges: Array<{ from: string; to: string }>,\n isDevDependency: boolean\n): void {\n // Check if node already exists\n if (!nodes.has(nodeId)) {\n nodes.set(nodeId, {\n id: nodeId,\n name: dep.name,\n version: dep.version,\n depth,\n parent,\n children: [],\n vulnerabilities: 0,\n isDevDependency,\n });\n }\n\n // Add edge from parent to this node\n if (parent) {\n edges.push({ from: parent, to: nodeId });\n const parentNode = nodes.get(parent);\n if (parentNode && !parentNode.children.includes(nodeId)) {\n parentNode.children.push(nodeId);\n }\n }\n\n // Process children\n if (dep.dependencies) {\n for (const [childName, childDep] of dep.dependencies) {\n const childId = `${childName}@${childDep.version}`;\n buildNodeRecursive(\n childDep,\n childId,\n depth + 1,\n nodeId,\n nodes,\n edges,\n isDevDependency\n );\n }\n }\n}\n\nexport function findDependencyPath(\n graph: DependencyGraph,\n packageName: string\n): string[][] {\n const paths: string[][] = [];\n\n for (const root of graph.roots) {\n const rootNode = graph.nodes.get(root);\n if (!rootNode) continue;\n\n if (rootNode.name === packageName) {\n paths.push([root]);\n } else {\n const subPaths = findPathsRecursive(graph, root, packageName, [root]);\n paths.push(...subPaths);\n }\n }\n\n return paths;\n}\n\nfunction findPathsRecursive(\n graph: DependencyGraph,\n currentNodeId: string,\n targetPackage: string,\n currentPath: string[]\n): string[][] {\n const paths: string[][] = [];\n const node = graph.nodes.get(currentNodeId);\n\n if (!node) {\n return paths;\n }\n\n for (const childId of node.children) {\n const childNode = graph.nodes.get(childId);\n if (!childNode) continue;\n\n const newPath = [...currentPath, childId];\n\n if (childNode.name === targetPackage) {\n paths.push(newPath);\n } else {\n const subPaths = findPathsRecursive(graph, childId, targetPackage, newPath);\n paths.push(...subPaths);\n }\n }\n\n return paths;\n}\n\nexport function getDepth(graph: DependencyGraph, nodeId: string): number {\n const node = graph.nodes.get(nodeId);\n return node?.depth ?? -1;\n}\n\nexport function getDirectDependencies(graph: DependencyGraph): GraphNode[] {\n return Array.from(graph.nodes.values()).filter(node => node.depth === 0);\n}\n\nexport function getTransitiveDependencies(graph: DependencyGraph): GraphNode[] {\n return Array.from(graph.nodes.values()).filter(node => node.depth > 0);\n}\n","import type { DependencyGraph } from './builder.js';\n\nexport interface VisualizationOptions {\n format: 'tree' | 'dot' | 'json';\n maxDepth?: number;\n showVersions?: boolean;\n highlightVulnerabilities?: boolean;\n}\n\nexport function visualize(\n graph: DependencyGraph,\n options: VisualizationOptions = { format: 'tree' }\n): string {\n switch (options.format) {\n case 'tree':\n return visualizeAsTree(graph, options);\n case 'dot':\n return visualizeAsDot(graph, options);\n case 'json':\n return visualizeAsJson(graph, options);\n default:\n throw new Error(`Unsupported format: ${options.format}`);\n }\n}\n\nfunction visualizeAsTree(graph: DependencyGraph, options: VisualizationOptions): string {\n const lines: string[] = [];\n const maxDepth = options.maxDepth ?? Infinity;\n const showVersions = options.showVersions ?? true;\n\n for (const rootId of graph.roots) {\n renderTreeNode(graph, rootId, 0, '', lines, maxDepth, showVersions, options.highlightVulnerabilities);\n }\n\n return lines.join('\\n');\n}\n\nfunction renderTreeNode(\n graph: DependencyGraph,\n nodeId: string,\n depth: number,\n prefix: string,\n lines: string[],\n maxDepth: number,\n showVersions: boolean,\n highlightVulns?: boolean\n): void {\n if (depth > maxDepth) {\n return;\n }\n\n const node = graph.nodes.get(nodeId);\n if (!node) return;\n\n const displayName = showVersions ? `${node.name}@${node.version}` : node.name;\n const vulnMarker = highlightVulns && node.vulnerabilities > 0 ? ` [${node.vulnerabilities} vuln(s)]` : '';\n const devMarker = node.isDevDependency ? ' (dev)' : '';\n\n lines.push(`${prefix}${displayName}${vulnMarker}${devMarker}`);\n\n const children = node.children;\n for (let i = 0; i < children.length; i++) {\n const isLast = i === children.length - 1;\n const childPrefix = prefix + (isLast ? '└── ' : '├── ');\n const childId = children[i];\n renderTreeNode(\n graph,\n childId,\n depth + 1,\n childPrefix,\n lines,\n maxDepth,\n showVersions,\n highlightVulns\n );\n }\n}\n\nfunction visualizeAsDot(graph: DependencyGraph, options: VisualizationOptions): string {\n const lines: string[] = [];\n lines.push('digraph dependencies {');\n lines.push(' rankdir=LR;');\n lines.push(' node [shape=box];');\n\n // Add nodes\n for (const [id, node] of graph.nodes) {\n const label = options.showVersions ? `${node.name}@${node.version}` : node.name;\n const color = node.vulnerabilities > 0 && options.highlightVulnerabilities ? 'red' : 'black';\n const style = node.isDevDependency ? 'dashed' : 'solid';\n\n lines.push(` \"${id}\" [label=\"${label}\", color=\"${color}\", style=\"${style}\"];`);\n }\n\n // Add edges\n for (const edge of graph.edges) {\n lines.push(` \"${edge.from}\" -> \"${edge.to}\";`);\n }\n\n lines.push('}');\n return lines.join('\\n');\n}\n\nfunction visualizeAsJson(graph: DependencyGraph, _options: VisualizationOptions): string {\n const data = {\n nodes: Array.from(graph.nodes.values()),\n edges: graph.edges,\n roots: graph.roots,\n };\n\n return JSON.stringify(data, null, 2);\n}\n\nexport function printSummary(graph: DependencyGraph): string {\n const totalNodes = graph.nodes.size;\n const directDeps = Array.from(graph.nodes.values()).filter(n => n.depth === 0).length;\n const transitiveDeps = totalNodes - directDeps;\n const devDeps = Array.from(graph.nodes.values()).filter(n => n.isDevDependency).length;\n const vulnNodes = Array.from(graph.nodes.values()).filter(n => n.vulnerabilities > 0).length;\n\n const lines = [\n 'Dependency Graph Summary',\n '========================',\n `Total packages: ${totalNodes}`,\n `Direct dependencies: ${directDeps}`,\n `Transitive dependencies: ${transitiveDeps}`,\n `Dev dependencies: ${devDeps}`,\n `Packages with vulnerabilities: ${vulnNodes}`,\n ];\n\n return lines.join('\\n');\n}\n","import { scanNpmProject, type ScanResult, type ScanOptions } from '../scanner/index.js';\nimport { analyzeBreakingChanges, type BreakingChangeAnalysis } from '../analyzer/index.js';\nimport { generateMigration, testDrivenUpdate, type Migration } from '../remediation/index.js';\nimport { findAlternatives, type Alternative, type SearchCriteria } from '../alternatives/index.js';\nimport { buildGraph, visualize, type DependencyGraph, type VisualizationOptions } from '../graph/index.js';\n\nexport interface SecureSyncOptions {\n projectPath: string;\n autoFix?: boolean;\n testBeforeUpdate?: boolean;\n createBackup?: boolean;\n}\n\nexport class SecureSync {\n private options: SecureSyncOptions;\n\n constructor(options: SecureSyncOptions) {\n this.options = {\n autoFix: false,\n testBeforeUpdate: true,\n createBackup: true,\n ...options,\n };\n }\n\n /**\n * Scan project dependencies for vulnerabilities\n */\n async scan(scanOptions?: Partial<ScanOptions>): Promise<ScanResult> {\n return scanNpmProject(this.options.projectPath, {\n projectPath: this.options.projectPath,\n ...scanOptions,\n });\n }\n\n /**\n * Analyze breaking changes for a package update\n */\n async analyzeBreakingChanges(\n packageName: string,\n fromVersion: string,\n toVersion: string\n ): Promise<BreakingChangeAnalysis> {\n return analyzeBreakingChanges(packageName, fromVersion, toVersion);\n }\n\n /**\n * Generate migration scripts for breaking changes\n */\n async generateMigrations(\n packageName: string,\n changes: BreakingChangeAnalysis\n ): Promise<Migration[]> {\n return generateMigration(\n this.options.projectPath,\n packageName,\n changes.changes\n );\n }\n\n /**\n * Auto-fix vulnerabilities with optional test-driven approach\n */\n async fix(options?: {\n maxSeverity?: 'low' | 'moderate' | 'high' | 'critical';\n breakingChanges?: 'skip' | 'warn' | 'allow';\n dryRun?: boolean;\n }): Promise<FixReport> {\n const scanResults = await this.scan();\n const maxSeverity = options?.maxSeverity || 'critical';\n const dryRun = options?.dryRun || false;\n\n const severityLevels = ['low', 'moderate', 'high', 'critical'];\n const maxSeverityIndex = severityLevels.indexOf(maxSeverity);\n\n const vulnsToFix = scanResults.vulnerabilities.filter(v => {\n const vulnIndex = severityLevels.indexOf(v.severity);\n return vulnIndex >= maxSeverityIndex;\n });\n\n const packageUpdates = new Map<string, { current: string; patched: string[] }>();\n for (const vuln of vulnsToFix) {\n if (!packageUpdates.has(vuln.package)) {\n packageUpdates.set(vuln.package, {\n current: vuln.version,\n patched: vuln.patched,\n });\n }\n }\n\n const results: FixResult[] = [];\n\n for (const [packageName, update] of packageUpdates) {\n const targetVersion = update.patched[0];\n if (!targetVersion) {\n results.push({\n package: packageName,\n success: false,\n reason: 'No patched version available',\n });\n continue;\n }\n\n // Analyze breaking changes\n const analysis = await this.analyzeBreakingChanges(\n packageName,\n update.current,\n targetVersion\n );\n\n // Handle breaking changes policy\n if (\n analysis.hasBreakingChanges &&\n options?.breakingChanges === 'skip'\n ) {\n results.push({\n package: packageName,\n success: false,\n reason: 'Breaking changes detected (skipped by policy)',\n breakingChanges: analysis,\n });\n continue;\n }\n\n // Generate migrations\n const migrations = await this.generateMigrations(packageName, analysis);\n\n if (dryRun) {\n results.push({\n package: packageName,\n success: true,\n dryRun: true,\n fromVersion: update.current,\n toVersion: targetVersion,\n migrations,\n breakingChanges: analysis.hasBreakingChanges ? analysis : undefined,\n });\n continue;\n }\n\n // Apply update\n const updateResult = await testDrivenUpdate(\n this.options.projectPath,\n packageName,\n targetVersion,\n migrations,\n {\n autoApply: this.options.autoFix,\n runTests: this.options.testBeforeUpdate,\n createBackup: this.options.createBackup,\n }\n );\n\n results.push({\n package: packageName,\n success: updateResult.success,\n reason: updateResult.reason,\n fromVersion: update.current,\n toVersion: targetVersion,\n migrations: updateResult.migrations,\n failedTests: updateResult.failedTests,\n rolledBack: updateResult.rolledBack,\n });\n }\n\n return {\n totalVulnerabilities: scanResults.vulnerabilities.length,\n vulnerabilitiesFixed: vulnsToFix.length,\n packagesUpdated: resul