dep-oracle 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -8
- package/dist/action/index.js +398 -16
- package/dist/badge-5Z3WAD2B.js +89 -0
- package/dist/badge-5Z3WAD2B.js.map +1 -0
- package/dist/chunk-32B3QIPY.js +1505 -0
- package/dist/chunk-32B3QIPY.js.map +1 -0
- package/dist/chunk-7DST6SNA.js +258 -0
- package/dist/chunk-7DST6SNA.js.map +1 -0
- package/dist/{chunk-TXSNFX3N.js → chunk-DLWG22RC.js} +403 -17
- package/dist/chunk-DLWG22RC.js.map +1 -0
- package/dist/chunk-HX6MGNBD.js +271 -0
- package/dist/chunk-HX6MGNBD.js.map +1 -0
- package/dist/chunk-IVXGOPRU.js +145 -0
- package/dist/chunk-IVXGOPRU.js.map +1 -0
- package/dist/chunk-SP3VYPXX.js +218 -0
- package/dist/chunk-SP3VYPXX.js.map +1 -0
- package/dist/chunk-T5EVLWZM.js +4234 -0
- package/dist/chunk-T5EVLWZM.js.map +1 -0
- package/dist/chunk-UMB5MJHL.js +239 -0
- package/dist/chunk-UMB5MJHL.js.map +1 -0
- package/dist/cli/index.js +163 -6499
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +9 -84
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +33 -12
- package/dist/mcp/server.js.map +1 -1
- package/dist/npm-UB54H37N.js +9 -0
- package/dist/npm-UB54H37N.js.map +1 -0
- package/dist/orchestrator-VOOYKDPT.js +8 -0
- package/dist/orchestrator-VOOYKDPT.js.map +1 -0
- package/dist/python-U4G2GK4J.js +9 -0
- package/dist/python-U4G2GK4J.js.map +1 -0
- package/dist/server-WONIBSG4.js +640 -0
- package/dist/server-WONIBSG4.js.map +1 -0
- package/dist/store-Z5UANEBB.js +8 -0
- package/dist/store-Z5UANEBB.js.map +1 -0
- package/dist/trust-score-YXYDFVPZ.js +8 -0
- package/dist/trust-score-YXYDFVPZ.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/chunk-TXSNFX3N.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/collectors/orchestrator.ts","../src/utils/logger.ts","../src/utils/rate-limiter.ts","../src/collectors/base.ts","../src/collectors/registry.ts","../src/collectors/pypi-registry.ts","../src/collectors/github.ts","../src/collectors/security.ts","../src/collectors/funding.ts","../src/collectors/popularity.ts","../src/collectors/license.ts"],"sourcesContent":["/**\n * CollectorOrchestrator -- coordinates all collectors and runs them in parallel\n * with controlled concurrency.\n *\n * Usage:\n * const orchestrator = new CollectorOrchestrator(cacheManager, { offline: false });\n * const results = await orchestrator.collectAll('express', '4.18.2');\n */\n\nimport pLimit from 'p-limit';\n\nimport type {\n CollectorResult,\n RegistryData,\n GitHubData,\n SecurityData,\n FundingData,\n PopularityData,\n LicenseData,\n} from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\n\nimport { RegistryCollector } from './registry.js';\nimport { PyPIRegistryCollector } from './pypi-registry.js';\nimport { GitHubCollector } from './github.js';\nimport { SecurityCollector } from './security.js';\nimport { FundingCollector } from './funding.js';\nimport { PopularityCollector } from './popularity.js';\nimport { LicenseCollector } from './license.js';\nimport type { BaseCollector } from './base.js';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface AllCollectorResults {\n registry: CollectorResult<RegistryData>;\n github: CollectorResult<GitHubData>;\n security: CollectorResult<SecurityData>;\n funding: CollectorResult<FundingData>;\n popularity: CollectorResult<PopularityData>;\n license: CollectorResult<LicenseData>;\n}\n\nexport interface OrchestratorOptions {\n /** When true, only cached data is returned. No network requests. */\n offline?: boolean;\n /** GitHub personal access token for higher rate limits. */\n githubToken?: string;\n /** Maximum concurrent collector tasks (default: 10). */\n concurrency?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\nexport class CollectorOrchestrator {\n private readonly cache: CacheManager;\n private readonly options: Required<OrchestratorOptions>;\n\n private readonly registryCollector: RegistryCollector;\n private readonly pypiRegistryCollector: PyPIRegistryCollector;\n private readonly githubCollector: GitHubCollector;\n private readonly securityCollector: SecurityCollector;\n private readonly fundingCollector: FundingCollector;\n private readonly popularityCollector: PopularityCollector;\n private readonly licenseCollector: LicenseCollector;\n\n constructor(cache: CacheManager, options: OrchestratorOptions = {}) {\n this.cache = cache;\n this.options = {\n offline: options.offline ?? false,\n githubToken: options.githubToken ?? process.env.GITHUB_TOKEN ?? '',\n concurrency: options.concurrency ?? 10,\n };\n\n this.registryCollector = new RegistryCollector(this.cache);\n this.pypiRegistryCollector = new PyPIRegistryCollector(this.cache);\n this.githubCollector = new GitHubCollector(this.cache, this.options.githubToken || undefined);\n this.securityCollector = new SecurityCollector(this.cache);\n this.fundingCollector = new FundingCollector(this.cache, this.options.githubToken || undefined);\n this.popularityCollector = new PopularityCollector(this.cache);\n this.licenseCollector = new LicenseCollector(this.cache);\n }\n\n /**\n * Run all collectors for the given package and version.\n *\n * In online mode every collector is invoked (cache-first). In offline mode\n * only the cache is consulted; if there is no cached entry the result gets\n * `status: 'offline'` with `data: null`.\n */\n async collectAll(\n packageName: string,\n version: string,\n ecosystem: 'npm' | 'pypi' = 'npm',\n ): Promise<AllCollectorResults> {\n logger.info(\n `Collecting data for ${packageName}@${version} (ecosystem=${ecosystem}, offline=${String(this.options.offline)})`,\n );\n\n const limit = pLimit(this.options.concurrency);\n\n type CollectorEntry<T> = {\n key: keyof AllCollectorResults;\n collector: BaseCollector<T>;\n };\n\n // Select the appropriate registry collector based on ecosystem\n const activeRegistryCollector =\n ecosystem === 'pypi'\n ? this.pypiRegistryCollector\n : this.registryCollector;\n\n // Map ecosystem to OSV ecosystem identifier\n const osvEcosystem = ecosystem === 'pypi' ? 'PyPI' : 'npm';\n\n // Type-safe collector list. We use `unknown` for the heterogeneous array\n // and cast at assignment time.\n const entries: Array<CollectorEntry<unknown>> = [\n { key: 'registry', collector: activeRegistryCollector as BaseCollector<unknown> },\n { key: 'github', collector: this.githubCollector as BaseCollector<unknown> },\n { key: 'security', collector: this.securityCollector as BaseCollector<unknown> },\n { key: 'funding', collector: this.fundingCollector as BaseCollector<unknown> },\n { key: 'popularity', collector: this.popularityCollector as BaseCollector<unknown> },\n { key: 'license', collector: this.licenseCollector as BaseCollector<unknown> },\n ];\n\n const results = {} as AllCollectorResults;\n\n const COLLECTOR_TIMEOUT = 30_000; // 30 seconds per collector\n\n const tasks = entries.map(({ key, collector }) =>\n limit(async () => {\n let result: CollectorResult<unknown>;\n\n if (this.options.offline) {\n result = await this.offlineCollect(collector, packageName, version);\n } else {\n try {\n // Pass ecosystem to the security collector\n if (key === 'security') {\n result = await Promise.race([\n this.securityCollector.collect(packageName, version, osvEcosystem),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Collector timeout')), COLLECTOR_TIMEOUT),\n ),\n ]);\n } else {\n result = await Promise.race([\n this.onlineCollect(collector, packageName, version),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Collector timeout')), COLLECTOR_TIMEOUT),\n ),\n ]);\n }\n } catch {\n logger.warn(`[${collector.name}] ${packageName}@${version} => timeout (${COLLECTOR_TIMEOUT}ms)`);\n result = {\n status: 'error',\n data: null,\n error: `Timeout after ${COLLECTOR_TIMEOUT / 1000}s`,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n logger.info(\n `[${collector.name}] ${packageName}@${version} => ${result.status}`,\n );\n\n (results as unknown as Record<string, CollectorResult<unknown>>)[key] = result;\n }),\n );\n\n await Promise.all(tasks);\n\n return results;\n }\n\n // ---------------------------------------------------------------------------\n // Online / Offline strategies\n // ---------------------------------------------------------------------------\n\n /**\n * Normal collection: delegate to the collector which checks cache internally.\n */\n private async onlineCollect<T>(\n collector: BaseCollector<T>,\n packageName: string,\n version: string,\n ): Promise<CollectorResult<T>> {\n try {\n return await collector.collect(packageName, version);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`Unhandled error in ${collector.name}: ${message}`);\n\n return {\n status: 'error',\n data: null,\n error: message,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n /**\n * Offline collection: only look in the cache. If nothing is cached return\n * a result with `status: 'offline'`.\n */\n private async offlineCollect<T>(\n collector: BaseCollector<T>,\n packageName: string,\n version: string,\n ): Promise<CollectorResult<T>> {\n try {\n const key = `${collector.name}:${packageName}@${version}`;\n const cached = await this.cache.get<T>(key);\n\n if (cached !== null && cached !== undefined) {\n logger.debug(`Offline cache hit: ${key}`);\n return {\n status: 'cached',\n data: cached,\n collectedAt: new Date().toISOString(),\n };\n }\n\n return {\n status: 'offline',\n data: null,\n collectedAt: new Date().toISOString(),\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Offline cache read failed for ${collector.name}: ${message}`);\n\n return {\n status: 'offline',\n data: null,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n}\n","import chalk from \"chalk\";\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nlet _verbose = false;\n\n/**\n * Enable or disable verbose (info-level) output.\n *\n * When verbose is off (default), only warn and error messages are printed.\n * Debug messages additionally require `DEP_ORACLE_DEBUG` to be set.\n */\nexport function setVerbose(enabled: boolean): void {\n _verbose = enabled;\n}\n\nexport function isVerbose(): boolean {\n return _verbose;\n}\n\n/**\n * Check whether debug output is enabled via the DEP_ORACLE_DEBUG env var.\n *\n * Any truthy value (\"1\", \"true\", \"yes\") enables debug logging.\n */\nexport function isDebug(): boolean {\n const val = process.env.DEP_ORACLE_DEBUG;\n if (!val) return false;\n return [\"1\", \"true\", \"yes\"].includes(val.toLowerCase());\n}\n\n// ---------------------------------------------------------------------------\n// Formatting helpers\n// ---------------------------------------------------------------------------\n\nfunction timestamp(): string {\n return new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm\n}\n\nfunction formatMessage(level: string, colorFn: (s: string) => string, msg: string): string {\n return `${chalk.dim(timestamp())} ${colorFn(level.padEnd(5))} ${msg}`;\n}\n\n// ---------------------------------------------------------------------------\n// Logger\n// ---------------------------------------------------------------------------\n\n/**\n * Structured logger for dep-oracle.\n *\n * - `debug`: gray, only printed when `DEP_ORACLE_DEBUG` is set\n * - `info`: blue, only printed when verbose mode is enabled or `DEP_ORACLE_DEBUG` is set\n * - `warn`: yellow, always printed\n * - `error`: red, always printed\n *\n * All output goes to stderr so it never contaminates piped JSON or table output.\n */\nexport const logger = {\n debug(msg: string): void {\n if (!isDebug()) return;\n process.stderr.write(formatMessage(\"DEBUG\", chalk.gray, chalk.gray(msg)) + \"\\n\");\n },\n\n info(msg: string): void {\n if (!_verbose && !isDebug()) return;\n process.stderr.write(formatMessage(\"INFO\", chalk.blue, msg) + \"\\n\");\n },\n\n warn(msg: string): void {\n process.stderr.write(formatMessage(\"WARN\", chalk.yellow, msg) + \"\\n\");\n },\n\n error(msg: string): void {\n process.stderr.write(formatMessage(\"ERROR\", chalk.red, msg) + \"\\n\");\n },\n} as const;\n\n/**\n * Create a child logger that prefixes every message with a label.\n *\n * Useful for per-module or per-collector logging:\n * ```ts\n * const log = createLogger(\"npm-collector\");\n * log.info(\"fetching registry data\"); // => 12:34:56.789 INFO [npm-collector] fetching registry data\n * ```\n */\nexport function createLogger(label: string) {\n const prefix = chalk.dim(`[${label}]`);\n return {\n debug(msg: string): void {\n logger.debug(`${prefix} ${msg}`);\n },\n info(msg: string): void {\n logger.info(`${prefix} ${msg}`);\n },\n warn(msg: string): void {\n logger.warn(`${prefix} ${msg}`);\n },\n error(msg: string): void {\n logger.error(`${prefix} ${msg}`);\n },\n } as const;\n}\n","/**\n * Token-bucket rate limiter for controlling outbound HTTP request frequency.\n *\n * Usage:\n * ```ts\n * const limiter = new RateLimiter(10, 60_000); // 10 requests per minute\n * await limiter.acquire(); // blocks if bucket is empty\n * await fetch(url);\n * ```\n */\nexport class RateLimiter {\n private tokens: number;\n private readonly maxTokens: number;\n private readonly refillIntervalMs: number;\n private lastRefill: number;\n private waitQueue: Array<() => void> = [];\n\n /**\n * @param maxRequests Maximum number of requests allowed in the window\n * @param windowMs Window duration in milliseconds\n */\n constructor(maxRequests: number, windowMs: number) {\n this.maxTokens = maxRequests;\n this.tokens = maxRequests;\n this.refillIntervalMs = windowMs;\n this.lastRefill = Date.now();\n }\n\n /**\n * Acquire a token. Resolves immediately when tokens are available,\n * otherwise waits until the bucket is refilled.\n */\n async acquire(): Promise<void> {\n this.refill();\n\n if (this.tokens > 0) {\n this.tokens--;\n return;\n }\n\n // No tokens available — wait for the next refill cycle\n return new Promise<void>((resolve) => {\n this.waitQueue.push(resolve);\n this.scheduleRefill();\n });\n }\n\n /**\n * Return the number of tokens currently available (without waiting).\n */\n get remaining(): number {\n this.refill();\n return this.tokens;\n }\n\n /**\n * Return the number of milliseconds until the next refill.\n */\n get msUntilRefill(): number {\n const elapsed = Date.now() - this.lastRefill;\n return Math.max(0, this.refillIntervalMs - elapsed);\n }\n\n // -----------------------------------------------------------------------\n // Internal\n // -----------------------------------------------------------------------\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefill;\n\n if (elapsed >= this.refillIntervalMs) {\n // Full refill\n const periods = Math.floor(elapsed / this.refillIntervalMs);\n this.tokens = Math.min(this.maxTokens, this.tokens + periods * this.maxTokens);\n this.lastRefill = now - (elapsed % this.refillIntervalMs);\n this.drainWaitQueue();\n }\n }\n\n private scheduleRefill(): void {\n const delay = this.msUntilRefill;\n if (delay <= 0) {\n this.refill();\n return;\n }\n\n setTimeout(() => {\n this.refill();\n }, delay);\n }\n\n private drainWaitQueue(): void {\n while (this.waitQueue.length > 0 && this.tokens > 0) {\n this.tokens--;\n const resolve = this.waitQueue.shift();\n resolve?.();\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Pre-configured limiters\n// ---------------------------------------------------------------------------\n\n/**\n * GitHub API rate limiter: 5000 requests per hour (authenticated).\n *\n * GitHub's unauthenticated limit is 60/hr, but dep-oracle is designed to\n * work with a token so we use the authenticated limit.\n */\nexport const githubRateLimiter = new RateLimiter(5000, 3_600_000);\n\n/**\n * npm registry rate limiter: generous default of 300 requests per minute.\n * The npm registry does not publish official limits, but this is safe.\n */\nexport const npmRateLimiter = new RateLimiter(300, 60_000);\n\n/**\n * PyPI rate limiter: conservative 100 requests per minute.\n */\nexport const pypiRateLimiter = new RateLimiter(100, 60_000);\n","/**\n * Abstract base class for all data collectors.\n *\n * Every collector extends BaseCollector<T> and returns CollectorResult<T>.\n * Provides transparent caching: subclasses call getCached / setCache and the\n * base takes care of serialisation through CacheManager.\n */\n\nimport type { CollectorResult } from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\n\nexport abstract class BaseCollector<T> {\n /** Human-readable collector name used in logs and cache keys. */\n abstract readonly name: string;\n\n protected readonly cache: CacheManager;\n\n /** Default cache TTL in seconds (24 hours). */\n protected readonly defaultTTL: number = 86_400;\n\n constructor(cache: CacheManager) {\n this.cache = cache;\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /** Collect data for the given package + version. */\n abstract collect(\n packageName: string,\n version: string,\n ): Promise<CollectorResult<T>>;\n\n // ---------------------------------------------------------------------------\n // Cache helpers\n // ---------------------------------------------------------------------------\n\n /** Build a deterministic cache key. */\n protected cacheKey(pkg: string, version: string): string {\n return `${this.name}:${pkg}@${version}`;\n }\n\n /**\n * Return a cached CollectorResult if one exists, otherwise `null`.\n *\n * When a cache hit is found the result is returned with `status: 'cached'`\n * so callers can differentiate between fresh and cached data.\n */\n protected async getCached(\n pkg: string,\n version: string,\n ): Promise<CollectorResult<T> | null> {\n const key = this.cacheKey(pkg, version);\n\n try {\n const cached = await this.cache.get<T>(key);\n if (cached !== null && cached !== undefined) {\n logger.debug(`Cache hit for ${key}`);\n return {\n status: 'cached',\n data: cached,\n collectedAt: new Date().toISOString(),\n };\n }\n } catch (err) {\n logger.warn(\n `Cache read failed for ${key}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n return null;\n }\n\n /**\n * Persist collector data in the cache.\n *\n * Failures are logged but never thrown -- caching is best-effort.\n */\n protected async setCache(\n pkg: string,\n version: string,\n data: T,\n ttl: number = this.defaultTTL,\n ): Promise<void> {\n const key = this.cacheKey(pkg, version);\n\n try {\n await this.cache.set<T>(key, data, ttl);\n logger.debug(`Cache set for ${key} (ttl=${ttl}s)`);\n } catch (err) {\n logger.warn(\n `Cache write failed for ${key}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n}\n","/**\n * RegistryCollector -- fetches package metadata from the npm registry.\n *\n * Data sources:\n * - https://registry.npmjs.org/{package} (metadata)\n * - https://api.npmjs.org/downloads/point/last-week/{package} (downloads)\n */\n\nimport type { CollectorResult, RegistryData } from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\nimport { npmRateLimiter } from '../utils/rate-limiter.js';\nimport { BaseCollector } from './base.js';\n\n/** Shape returned by the npm downloads API. */\ninterface NpmDownloadsResponse {\n downloads: number;\n start: string;\n end: string;\n package: string;\n}\n\n/** Subset of the npm registry packument we actually use. */\ninterface NpmPackument {\n name: string;\n description?: string;\n 'dist-tags'?: Record<string, string>;\n time?: Record<string, string>;\n versions?: Record<string, NpmVersionInfo>;\n repository?: { type?: string; url?: string } | string;\n license?: string;\n}\n\ninterface NpmVersionInfo {\n deprecated?: string;\n [key: string]: unknown;\n}\n\nexport class RegistryCollector extends BaseCollector<RegistryData> {\n readonly name = 'registry';\n\n constructor(cache: CacheManager) {\n super(cache);\n }\n\n async collect(\n packageName: string,\n version: string,\n ): Promise<CollectorResult<RegistryData>> {\n // Check cache first\n const cached = await this.getCached(packageName, version);\n if (cached) return cached;\n\n try {\n const [packument, downloads] = await Promise.all([\n this.fetchPackument(packageName),\n this.fetchWeeklyDownloads(packageName),\n ]);\n\n const versionCount = packument.versions\n ? Object.keys(packument.versions).length\n : 0;\n\n // Determine last publish date from the time map\n const timeEntries = packument.time ?? {};\n const publishDates = Object.entries(timeEntries)\n .filter(([key]) => key !== 'created' && key !== 'modified')\n .map(([, value]) => new Date(value).getTime())\n .sort((a, b) => b - a);\n\n const lastPublishDate = publishDates.length > 0\n ? new Date(publishDates[0]).toISOString()\n : null;\n\n // Check if the requested version is deprecated\n const versionInfo = packument.versions?.[version];\n const deprecated = versionInfo?.deprecated\n ? String(versionInfo.deprecated)\n : null;\n\n const data: RegistryData = {\n packageName,\n version,\n description: packument.description ?? null,\n lastPublishDate,\n versionCount,\n deprecated,\n weeklyDownloads: downloads,\n license: packument.license ?? null,\n repositoryUrl: this.extractRepoUrl(packument),\n };\n\n await this.setCache(packageName, version, data);\n\n return {\n status: 'success',\n data,\n collectedAt: new Date().toISOString(),\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`RegistryCollector failed for ${packageName}@${version}: ${message}`);\n\n return {\n status: 'error',\n data: null,\n error: message,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private async fetchPackument(packageName: string): Promise<NpmPackument> {\n const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;\n logger.debug(`Fetching packument: ${url}`);\n\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) {\n throw new Error(`npm registry returned ${res.status} for ${packageName}`);\n }\n\n return (await res.json()) as NpmPackument;\n }\n\n private async fetchWeeklyDownloads(packageName: string): Promise<number> {\n const url = `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`;\n logger.debug(`Fetching weekly downloads: ${url}`);\n\n try {\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) {\n logger.warn(`Downloads API returned ${res.status} for ${packageName}`);\n return 0;\n }\n\n const body = (await res.json()) as NpmDownloadsResponse;\n return body.downloads ?? 0;\n } catch {\n logger.warn(`Could not fetch download stats for ${packageName}`);\n return 0;\n }\n }\n\n /**\n * Extract a normalised GitHub/repo URL from the packument.\n * npm stores repo URLs in several formats; we normalise to https.\n */\n private extractRepoUrl(packument: NpmPackument): string | null {\n const repo = packument.repository;\n if (!repo) return null;\n\n const raw = typeof repo === 'string' ? repo : repo.url;\n if (!raw) return null;\n\n // Normalise git+https://..., git://..., git+ssh://git@github.com/... etc.\n return raw\n .replace(/^git\\+/, '')\n .replace(/^git:\\/\\//, 'https://')\n .replace(/^ssh:\\/\\/git@github\\.com/, 'https://github.com')\n .replace(/^git@github\\.com:/, 'https://github.com/')\n .replace(/\\.git$/, '');\n }\n}\n","/**\n * PyPIRegistryCollector -- fetches package metadata from the PyPI registry.\n *\n * Data sources:\n * - https://pypi.org/pypi/{package}/json (metadata)\n * - https://pypistats.org/api/packages/{package}/recent (downloads)\n */\n\nimport type { CollectorResult, RegistryData } from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\nimport { pypiRateLimiter } from '../utils/rate-limiter.js';\nimport { BaseCollector } from './base.js';\n\n/** Subset of the PyPI JSON API response we actually use. */\ninterface PyPIPackageInfo {\n name: string;\n summary?: string;\n version: string;\n license?: string;\n project_urls?: Record<string, string>;\n home_page?: string;\n}\n\ninterface PyPIRelease {\n upload_time_iso_8601?: string;\n upload_time?: string;\n yanked?: boolean;\n yanked_reason?: string;\n}\n\ninterface PyPIResponse {\n info: PyPIPackageInfo;\n releases?: Record<string, PyPIRelease[]>;\n}\n\n/** Shape returned by the pypistats recent downloads API. */\ninterface PyPIStatsResponse {\n data?: {\n last_week?: number;\n last_month?: number;\n last_day?: number;\n };\n}\n\nexport class PyPIRegistryCollector extends BaseCollector<RegistryData> {\n readonly name = 'pypi-registry';\n\n constructor(cache: CacheManager) {\n super(cache);\n }\n\n async collect(\n packageName: string,\n version: string,\n ): Promise<CollectorResult<RegistryData>> {\n // Check cache first\n const cached = await this.getCached(packageName, version);\n if (cached) return cached;\n\n try {\n const [metadataResult, downloadsResult] = await Promise.allSettled([\n this.fetchMetadata(packageName),\n this.fetchWeeklyDownloads(packageName),\n ]);\n\n const metadata =\n metadataResult.status === 'fulfilled' ? metadataResult.value : null;\n const downloads =\n downloadsResult.status === 'fulfilled' ? downloadsResult.value : 0;\n\n if (!metadata) {\n throw new Error(`PyPI registry returned no data for ${packageName}`);\n }\n\n const versionCount = metadata.releases\n ? Object.keys(metadata.releases).length\n : 0;\n\n // Determine last publish date from releases\n const lastPublishDate = this.findLastPublishDate(metadata.releases);\n\n // Check if the requested version is yanked (PyPI equivalent of deprecated)\n const deprecated = this.checkYanked(metadata.releases, version);\n\n const data: RegistryData = {\n packageName,\n version: version === 'latest' ? metadata.info.version : version,\n description: metadata.info.summary ?? null,\n lastPublishDate,\n versionCount,\n deprecated,\n weeklyDownloads: downloads,\n license: metadata.info.license ?? null,\n repositoryUrl: this.extractRepoUrl(metadata.info),\n };\n\n await this.setCache(packageName, version, data);\n\n return {\n status: 'success',\n data,\n collectedAt: new Date().toISOString(),\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(\n `PyPIRegistryCollector failed for ${packageName}@${version}: ${message}`,\n );\n\n return {\n status: 'error',\n data: null,\n error: message,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private async fetchMetadata(\n packageName: string,\n ): Promise<PyPIResponse | null> {\n await pypiRateLimiter.acquire();\n\n const url = `https://pypi.org/pypi/${encodeURIComponent(packageName)}/json`;\n logger.debug(`Fetching PyPI metadata: ${url}`);\n\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) {\n if (res.status === 404) return null;\n throw new Error(`PyPI registry returned ${res.status} for ${packageName}`);\n }\n\n return (await res.json()) as PyPIResponse;\n }\n\n private async fetchWeeklyDownloads(packageName: string): Promise<number> {\n await pypiRateLimiter.acquire();\n\n const url = `https://pypistats.org/api/packages/${encodeURIComponent(packageName)}/recent`;\n logger.debug(`Fetching PyPI weekly downloads: ${url}`);\n\n try {\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) {\n logger.warn(\n `PyPI stats API returned ${res.status} for ${packageName}`,\n );\n return 0;\n }\n\n const body = (await res.json()) as PyPIStatsResponse;\n return body.data?.last_week ?? 0;\n } catch {\n logger.warn(`Could not fetch PyPI download stats for ${packageName}`);\n return 0;\n }\n }\n\n /**\n * Find the most recent upload date across all releases.\n */\n private findLastPublishDate(\n releases: Record<string, PyPIRelease[]> | undefined,\n ): string | null {\n if (!releases) return null;\n\n const dates: number[] = [];\n\n for (const files of Object.values(releases)) {\n for (const file of files) {\n const dateStr = file.upload_time_iso_8601 ?? file.upload_time;\n if (dateStr) {\n const ts = new Date(dateStr).getTime();\n if (!isNaN(ts)) dates.push(ts);\n }\n }\n }\n\n if (dates.length === 0) return null;\n\n dates.sort((a, b) => b - a);\n return new Date(dates[0]).toISOString();\n }\n\n /**\n * Check if the requested version is yanked (PyPI's deprecation mechanism).\n * Returns the yank reason string, or null if not yanked.\n */\n private checkYanked(\n releases: Record<string, PyPIRelease[]> | undefined,\n version: string,\n ): string | null {\n if (!releases || version === 'latest') return null;\n\n const files = releases[version];\n if (!files || files.length === 0) return null;\n\n // A version is considered yanked if any of its files are yanked\n const yankedFile = files.find((f) => f.yanked);\n if (yankedFile) {\n return yankedFile.yanked_reason || 'This version has been yanked';\n }\n\n return null;\n }\n\n /**\n * Extract a normalised repository URL from PyPI project_urls or home_page.\n */\n private extractRepoUrl(info: PyPIPackageInfo): string | null {\n const projectUrls = info.project_urls ?? {};\n\n // PyPI project_urls commonly use these keys for source code\n const repoKeys = [\n 'Source',\n 'Source Code',\n 'Repository',\n 'GitHub',\n 'Code',\n 'Homepage',\n 'source',\n 'source_code',\n 'repository',\n 'github',\n 'code',\n 'homepage',\n ];\n\n for (const key of repoKeys) {\n const url = projectUrls[key];\n if (url && (url.includes('github.com') || url.includes('gitlab.com') || url.includes('bitbucket.org'))) {\n return url.replace(/\\.git$/, '');\n }\n }\n\n // Fallback: check all project_urls for a GitHub/GitLab/Bitbucket link\n for (const url of Object.values(projectUrls)) {\n if (url && (url.includes('github.com') || url.includes('gitlab.com') || url.includes('bitbucket.org'))) {\n return url.replace(/\\.git$/, '');\n }\n }\n\n // Last resort: home_page\n const homePage = info.home_page;\n if (homePage && (homePage.includes('github.com') || homePage.includes('gitlab.com'))) {\n return homePage.replace(/\\.git$/, '');\n }\n\n return null;\n }\n}\n","/**\n * GitHubCollector -- fetches repository health metrics from the GitHub REST API.\n *\n * Data sources:\n * - /repos/{owner}/{repo} (stars, forks, issues)\n * - /repos/{owner}/{repo}/contributors (contributor count via Link header)\n * - /repos/{owner}/{repo}/commits (recent activity, latest commit)\n * - /repos/{owner}/{repo}/contents/.github/FUNDING.yml (sponsor / funding info)\n *\n * An optional GITHUB_TOKEN env var is used to raise rate limits from 60 to\n * 5 000 requests / hour.\n */\n\nimport type { CollectorResult, GitHubData } from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\nimport { githubRateLimiter, npmRateLimiter } from '../utils/rate-limiter.js';\nimport { BaseCollector } from './base.js';\n\ninterface GitHubRepoResponse {\n stargazers_count: number;\n forks_count: number;\n open_issues_count: number;\n updated_at: string;\n archived: boolean;\n disabled: boolean;\n default_branch: string;\n}\n\ninterface GitHubCommitItem {\n sha: string;\n commit: {\n committer: { date: string } | null;\n message: string;\n };\n}\n\nexport class GitHubCollector extends BaseCollector<GitHubData> {\n readonly name = 'github';\n\n private readonly token: string | undefined;\n\n constructor(cache: CacheManager, githubToken?: string) {\n super(cache);\n this.token = githubToken ?? process.env.GITHUB_TOKEN;\n }\n\n async collect(\n packageName: string,\n version: string,\n ): Promise<CollectorResult<GitHubData>> {\n // Check cache first\n const cached = await this.getCached(packageName, version);\n if (cached) return cached;\n\n try {\n const repoSlug = await this.resolveRepoSlug(packageName);\n\n if (!repoSlug) {\n return {\n status: 'error',\n data: null,\n error: 'No GitHub repository found',\n collectedAt: new Date().toISOString(),\n };\n }\n\n const { owner, repo } = repoSlug;\n\n // Fire all requests in parallel -- each one is independently safe.\n const [repoInfo, contributorCount, recentCommitCount, latestCommit, hasFunding] =\n await Promise.all([\n this.fetchRepoInfo(owner, repo),\n this.fetchContributorCount(owner, repo),\n this.fetchRecentCommitCount(owner, repo),\n this.fetchLatestCommit(owner, repo),\n this.checkFundingYml(owner, repo),\n ]);\n\n const data: GitHubData = {\n owner,\n repo,\n stars: repoInfo.stargazers_count,\n forks: repoInfo.forks_count,\n openIssues: repoInfo.open_issues_count,\n updatedAt: repoInfo.updated_at,\n archived: repoInfo.archived,\n defaultBranch: repoInfo.default_branch,\n contributorCount,\n recentCommitCount,\n lastCommitDate: latestCommit\n ? latestCommit.commit.committer?.date ?? null\n : null,\n lastCommitSha: latestCommit?.sha ?? null,\n hasFundingYml: hasFunding,\n };\n\n await this.setCache(packageName, version, data);\n\n return {\n status: 'success',\n data,\n collectedAt: new Date().toISOString(),\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`GitHubCollector failed for ${packageName}@${version}: ${message}`);\n\n return {\n status: 'error',\n data: null,\n error: message,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n // ---------------------------------------------------------------------------\n // Repo slug resolution\n // ---------------------------------------------------------------------------\n\n /**\n * Determine the GitHub owner/repo from the npm registry metadata.\n * Falls back to a well-known heuristic for scoped packages.\n */\n private async resolveRepoSlug(\n packageName: string,\n ): Promise<{ owner: string; repo: string } | null> {\n try {\n const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) return null;\n\n const body = (await res.json()) as {\n repository?: { url?: string } | string;\n };\n\n const repoField = body.repository;\n const raw = typeof repoField === 'string' ? repoField : repoField?.url;\n\n if (!raw) return null;\n\n return this.parseGitHubUrl(raw);\n } catch {\n return null;\n }\n }\n\n /** Extract owner/repo from a variety of GitHub URL formats. */\n private parseGitHubUrl(\n raw: string,\n ): { owner: string; repo: string } | null {\n const normalised = raw\n .replace(/^git\\+/, '')\n .replace(/^git:\\/\\//, 'https://')\n .replace(/^ssh:\\/\\/git@github\\.com/, 'https://github.com')\n .replace(/^git@github\\.com:/, 'https://github.com/')\n .replace(/\\.git$/, '');\n\n const match = normalised.match(\n /github\\.com\\/([^/]+)\\/([^/]+)/,\n );\n\n if (!match) return null;\n\n const owner = match[1];\n const repo = match[2];\n // Validate GitHub owner/repo format\n const GITHUB_NAME = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/;\n if (!GITHUB_NAME.test(owner) || !GITHUB_NAME.test(repo)) return null;\n\n return { owner, repo };\n }\n\n // ---------------------------------------------------------------------------\n // GitHub API calls\n // ---------------------------------------------------------------------------\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = {\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n };\n if (this.token) {\n h.Authorization = `Bearer ${this.token}`;\n }\n return h;\n }\n\n private async fetchRepoInfo(\n owner: string,\n repo: string,\n ): Promise<GitHubRepoResponse> {\n const url = `https://api.github.com/repos/${owner}/${repo}`;\n logger.debug(`GitHub: fetching repo info ${url}`);\n\n await githubRateLimiter.acquire();\n const res = await fetch(url, { headers: this.headers() });\n if (!res.ok) {\n throw new Error(`GitHub API ${res.status} for ${url}`);\n }\n return (await res.json()) as GitHubRepoResponse;\n }\n\n /**\n * Get total contributor count using the Link header pagination trick.\n * We request per_page=1&anon=true and read the `last` page number.\n */\n private async fetchContributorCount(\n owner: string,\n repo: string,\n ): Promise<number> {\n const url = `https://api.github.com/repos/${owner}/${repo}/contributors?per_page=1&anon=true`;\n logger.debug(`GitHub: fetching contributor count ${url}`);\n\n try {\n await githubRateLimiter.acquire();\n const res = await fetch(url, { headers: this.headers() });\n if (!res.ok) return 0;\n\n const count = this.extractLastPage(res.headers.get('link'));\n // If there is no Link header the repo has a single page of contributors.\n if (count !== null) return count;\n\n // Fallback: count items in the response body.\n const body = (await res.json()) as unknown[];\n return body.length;\n } catch {\n return 0;\n }\n }\n\n /**\n * Count commits in the last 30 days via the same Link-header trick.\n */\n private async fetchRecentCommitCount(\n owner: string,\n repo: string,\n ): Promise<number> {\n const since = new Date(\n Date.now() - 30 * 24 * 60 * 60 * 1000,\n ).toISOString();\n\n const url = `https://api.github.com/repos/${owner}/${repo}/commits?since=${since}&per_page=1`;\n logger.debug(`GitHub: fetching recent commit count ${url}`);\n\n try {\n await githubRateLimiter.acquire();\n const res = await fetch(url, { headers: this.headers() });\n if (!res.ok) return 0;\n\n const count = this.extractLastPage(res.headers.get('link'));\n if (count !== null) return count;\n\n const body = (await res.json()) as unknown[];\n return body.length;\n } catch {\n return 0;\n }\n }\n\n /** Fetch the single most-recent commit. */\n private async fetchLatestCommit(\n owner: string,\n repo: string,\n ): Promise<GitHubCommitItem | null> {\n const url = `https://api.github.com/repos/${owner}/${repo}/commits?per_page=1`;\n logger.debug(`GitHub: fetching latest commit ${url}`);\n\n try {\n await githubRateLimiter.acquire();\n const res = await fetch(url, { headers: this.headers() });\n if (!res.ok) return null;\n\n const body = (await res.json()) as GitHubCommitItem[];\n return body[0] ?? null;\n } catch {\n return null;\n }\n }\n\n /**\n * Check whether the repository contains a .github/FUNDING.yml file.\n * A 200 response means the file exists.\n */\n private async checkFundingYml(\n owner: string,\n repo: string,\n ): Promise<boolean> {\n const url = `https://api.github.com/repos/${owner}/${repo}/contents/.github/FUNDING.yml`;\n logger.debug(`GitHub: checking FUNDING.yml ${url}`);\n\n try {\n await githubRateLimiter.acquire();\n const res = await fetch(url, { headers: this.headers() });\n return res.ok;\n } catch {\n return false;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Utilities\n // ---------------------------------------------------------------------------\n\n /**\n * Parse the GitHub `Link` header and return the last page number.\n *\n * Link: <...?page=42>; rel=\"last\", <...?page=2>; rel=\"next\"\n *\n * Returns `null` when there is no last page (single page of results).\n */\n private extractLastPage(linkHeader: string | null): number | null {\n if (!linkHeader) return null;\n\n const match = linkHeader.match(\n /[?&]page=(\\d+)[^>]*>;\\s*rel=\"last\"/,\n );\n\n return match ? parseInt(match[1], 10) : null;\n }\n}\n","/**\n * SecurityCollector -- queries the OSV.dev API for known vulnerabilities\n * affecting an npm package.\n *\n * Data source:\n * POST https://api.osv.dev/v1/query\n */\n\nimport type { CollectorResult, SecurityData } from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\nimport { BaseCollector } from './base.js';\n\n/** Subset of the OSV vulnerability schema we use. */\ninterface OsvVulnerability {\n id: string;\n summary?: string;\n modified: string;\n published?: string;\n severity?: OsvSeverity[];\n database_specific?: {\n severity?: string;\n [key: string]: unknown;\n };\n affected?: OsvAffected[];\n}\n\ninterface OsvSeverity {\n type: string;\n score: string;\n}\n\ninterface OsvAffected {\n package?: { name?: string; ecosystem?: string };\n ranges?: OsvRange[];\n versions?: string[];\n}\n\ninterface OsvRange {\n type: string;\n events: Array<{ introduced?: string; fixed?: string }>;\n}\n\ninterface OsvQueryResponse {\n vulns?: OsvVulnerability[];\n}\n\ntype SeverityLevel = 'critical' | 'high' | 'medium' | 'low' | 'unknown';\n\nexport class SecurityCollector extends BaseCollector<SecurityData> {\n readonly name = 'security';\n\n constructor(cache: CacheManager) {\n super(cache);\n }\n\n async collect(\n packageName: string,\n version: string,\n ecosystem?: string,\n ): Promise<CollectorResult<SecurityData>> {\n // Check cache first\n const cached = await this.getCached(packageName, version);\n if (cached) return cached;\n\n try {\n const vulns = await this.queryOsv(packageName, ecosystem);\n\n const totalVulnerabilities = vulns.length;\n\n // Count vulnerabilities by severity\n const severityCounts: Record<SeverityLevel, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n unknown: 0,\n };\n\n for (const vuln of vulns) {\n const severity = this.extractSeverity(vuln);\n severityCounts[severity]++;\n }\n\n // Find the latest vulnerability date\n const vulnDates = vulns\n .map((v) => new Date(v.published ?? v.modified).getTime())\n .filter((t) => !isNaN(t))\n .sort((a, b) => b - a);\n\n const latestVulnDate = vulnDates.length > 0\n ? new Date(vulnDates[0]).toISOString()\n : null;\n\n // Estimate average patch time in days.\n // For each vuln that has a \"fixed\" event we compute introduced -> fixed delta.\n const patchDays = this.estimateAveragePatchDays(vulns);\n\n const data: SecurityData = {\n packageName,\n version,\n totalVulnerabilities,\n severityCounts,\n latestVulnDate,\n averagePatchDays: patchDays,\n vulnerabilities: vulns.map((v) => ({\n id: v.id,\n summary: v.summary ?? null,\n severity: this.extractSeverity(v),\n published: v.published ?? v.modified,\n })),\n };\n\n await this.setCache(packageName, version, data);\n\n return {\n status: 'success',\n data,\n collectedAt: new Date().toISOString(),\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`SecurityCollector failed for ${packageName}@${version}: ${message}`);\n\n return {\n status: 'error',\n data: null,\n error: message,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n // ---------------------------------------------------------------------------\n // OSV API\n // ---------------------------------------------------------------------------\n\n private async queryOsv(packageName: string, ecosystem: string = 'npm'): Promise<OsvVulnerability[]> {\n const url = 'https://api.osv.dev/v1/query';\n logger.debug(`OSV: querying vulnerabilities for ${packageName} (ecosystem=${ecosystem})`);\n\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n package: {\n name: packageName,\n ecosystem,\n },\n }),\n });\n\n if (!res.ok) {\n throw new Error(`OSV API returned ${res.status}`);\n }\n\n const body = (await res.json()) as OsvQueryResponse;\n return body.vulns ?? [];\n }\n\n // ---------------------------------------------------------------------------\n // Severity extraction\n // ---------------------------------------------------------------------------\n\n /**\n * Determine the highest severity label for a vulnerability.\n *\n * OSV may provide CVSS vectors, database-specific severity strings, or\n * nothing at all. We try multiple sources in order of preference.\n */\n private extractSeverity(vuln: OsvVulnerability): SeverityLevel {\n // 1. Try CVSS score from severity array\n if (vuln.severity && vuln.severity.length > 0) {\n for (const s of vuln.severity) {\n const level = this.cvssToLevel(s.score);\n if (level !== 'unknown') return level;\n }\n }\n\n // 2. Try database_specific.severity string\n const dbSeverity = vuln.database_specific?.severity;\n if (typeof dbSeverity === 'string') {\n const normalised = dbSeverity.toLowerCase().trim();\n if (normalised === 'critical') return 'critical';\n if (normalised === 'high') return 'high';\n if (normalised === 'moderate' || normalised === 'medium') return 'medium';\n if (normalised === 'low') return 'low';\n }\n\n return 'unknown';\n }\n\n /**\n * Map a CVSS 3.x vector string to a severity level by extracting the\n * base score. If the string looks like a plain number, use it directly.\n */\n private cvssToLevel(scoreOrVector: string): SeverityLevel {\n let numeric: number;\n\n if (scoreOrVector.startsWith('CVSS:')) {\n // Extract base score: it is not embedded literally in the vector, so\n // we fall back to a simpler heuristic based on the Attack Complexity\n // and Impact metrics. A proper CVSS calculator is out of scope here,\n // so we return 'unknown' and let database_specific take over.\n return 'unknown';\n }\n\n numeric = parseFloat(scoreOrVector);\n if (isNaN(numeric)) return 'unknown';\n\n if (numeric >= 9.0) return 'critical';\n if (numeric >= 7.0) return 'high';\n if (numeric >= 4.0) return 'medium';\n if (numeric > 0) return 'low';\n\n return 'unknown';\n }\n\n // ---------------------------------------------------------------------------\n // Patch-time estimation\n // ---------------------------------------------------------------------------\n\n /**\n * Estimate average number of days between a vulnerability being introduced\n * and being fixed. Uses the range events in the OSV affected data.\n *\n * When no usable data is available, returns `null`.\n */\n private estimateAveragePatchDays(vulns: OsvVulnerability[]): number | null {\n const daysPerVuln: number[] = [];\n\n for (const vuln of vulns) {\n if (!vuln.affected) continue;\n\n for (const affected of vuln.affected) {\n if (!affected.ranges) continue;\n\n for (const range of affected.ranges) {\n let introduced: string | undefined;\n let fixed: string | undefined;\n\n for (const event of range.events) {\n if (event.introduced) introduced = event.introduced;\n if (event.fixed) fixed = event.fixed;\n }\n\n // We can only compute a meaningful delta when we have dates\n // (ECOSYSTEM or SEMVER ranges use version strings, not dates).\n // For GIT ranges the events are commit SHAs.\n // Fall back to published -> modified as a proxy.\n if (introduced && fixed) {\n // These are typically version strings, not dates. Skip.\n continue;\n }\n }\n }\n\n // Proxy: time from published to last modified (often the fix date).\n const published = vuln.published ? new Date(vuln.published).getTime() : NaN;\n const modified = new Date(vuln.modified).getTime();\n\n if (!isNaN(published) && !isNaN(modified) && modified > published) {\n const days = (modified - published) / (1000 * 60 * 60 * 24);\n if (days > 0 && days < 3650) {\n // Sanity: ignore anything over 10 years\n daysPerVuln.push(days);\n }\n }\n }\n\n if (daysPerVuln.length === 0) return null;\n\n const total = daysPerVuln.reduce((sum, d) => sum + d, 0);\n return Math.round(total / daysPerVuln.length);\n }\n}\n","/**\n * FundingCollector -- best-effort detection of project funding channels.\n *\n * Data sources:\n * - GitHub FUNDING.yml (via GitHub API)\n * - OpenCollective public profile JSON\n * - npm registry \"funding\" field\n *\n * Many packages will have no funding info at all; the collector returns\n * sensible defaults in that case and never throws.\n */\n\nimport type { CollectorResult, FundingData } from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\nimport { githubRateLimiter, npmRateLimiter } from '../utils/rate-limiter.js';\nimport { BaseCollector } from './base.js';\n\n/** Simplified OpenCollective profile shape. */\ninterface OpenCollectiveProfile {\n slug: string;\n name?: string;\n currency?: string;\n balance?: number;\n yearlyBudget?: number;\n backersCount?: number;\n contributorsCount?: number;\n isActive?: boolean;\n}\n\nexport class FundingCollector extends BaseCollector<FundingData> {\n readonly name = 'funding';\n\n private readonly githubToken: string | undefined;\n\n constructor(cache: CacheManager, githubToken?: string) {\n super(cache);\n this.githubToken = githubToken ?? process.env.GITHUB_TOKEN;\n }\n\n async collect(\n packageName: string,\n version: string,\n ): Promise<CollectorResult<FundingData>> {\n // Check cache first\n const cached = await this.getCached(packageName, version);\n if (cached) return cached;\n\n try {\n // Gather information from multiple sources in parallel.\n const [repoSlug, npmFunding] = await Promise.all([\n this.resolveRepoSlug(packageName),\n this.fetchNpmFunding(packageName),\n ]);\n\n // Fire secondary lookups in parallel once we know the repo slug.\n const [fundingYml, openCollective] = await Promise.all([\n repoSlug\n ? this.checkFundingYml(repoSlug.owner, repoSlug.repo)\n : Promise.resolve(null),\n this.fetchOpenCollective(packageName),\n ]);\n\n const hasSponsors = fundingYml !== null && fundingYml.length > 0;\n const hasOpenCollective = openCollective !== null && (openCollective.isActive ?? false);\n const hasNpmFunding = npmFunding !== null;\n\n // Rough funding estimate based on available signals.\n let estimatedAnnualFunding = 0;\n if (openCollective?.yearlyBudget) {\n // OpenCollective reports in cents for some currencies.\n estimatedAnnualFunding = openCollective.yearlyBudget > 1_000_000\n ? Math.round(openCollective.yearlyBudget / 100)\n : openCollective.yearlyBudget;\n }\n\n const data: FundingData = {\n packageName,\n hasSponsors,\n hasOpenCollective,\n hasNpmFunding,\n openCollectiveSlug: openCollective?.slug ?? null,\n openCollectiveBackers: openCollective?.backersCount ?? 0,\n estimatedAnnualFunding,\n fundingUrls: this.buildFundingUrls(npmFunding, fundingYml, openCollective),\n };\n\n await this.setCache(packageName, version, data);\n\n return {\n status: 'success',\n data,\n collectedAt: new Date().toISOString(),\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`FundingCollector failed for ${packageName}@${version}: ${message}`);\n\n // Return defaults -- funding is entirely optional.\n const data: FundingData = {\n packageName,\n hasSponsors: false,\n hasOpenCollective: false,\n hasNpmFunding: false,\n openCollectiveSlug: null,\n openCollectiveBackers: 0,\n estimatedAnnualFunding: 0,\n fundingUrls: [],\n };\n\n return {\n status: 'success',\n data,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n // ---------------------------------------------------------------------------\n // npm registry funding field\n // ---------------------------------------------------------------------------\n\n private async fetchNpmFunding(\n packageName: string,\n ): Promise<string | string[] | null> {\n try {\n const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) return null;\n\n const body = (await res.json()) as {\n funding?: string | { url?: string } | Array<string | { url?: string }>;\n };\n\n if (!body.funding) return null;\n\n if (typeof body.funding === 'string') return body.funding;\n if (Array.isArray(body.funding)) {\n return body.funding.map((f) =>\n typeof f === 'string' ? f : f.url ?? '',\n ).filter(Boolean);\n }\n if (typeof body.funding === 'object' && body.funding.url) {\n return body.funding.url;\n }\n\n return null;\n } catch {\n return null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // GitHub FUNDING.yml\n // ---------------------------------------------------------------------------\n\n private async checkFundingYml(\n owner: string,\n repo: string,\n ): Promise<string | null> {\n const url = `https://api.github.com/repos/${owner}/${repo}/contents/.github/FUNDING.yml`;\n logger.debug(`Funding: checking FUNDING.yml ${url}`);\n\n try {\n const headers: Record<string, string> = {\n Accept: 'application/vnd.github.raw+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n };\n if (this.githubToken) {\n headers.Authorization = `Bearer ${this.githubToken}`;\n }\n\n await githubRateLimiter.acquire();\n const res = await fetch(url, { headers });\n if (!res.ok) return null;\n\n return await res.text();\n } catch {\n return null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // OpenCollective\n // ---------------------------------------------------------------------------\n\n private async fetchOpenCollective(\n packageName: string,\n ): Promise<OpenCollectiveProfile | null> {\n // OpenCollective slugs are typically the package name (without scope).\n const slug = packageName.replace(/^@[^/]+\\//, '');\n const url = `https://opencollective.com/${encodeURIComponent(slug)}.json`;\n logger.debug(`Funding: checking OpenCollective ${url}`);\n\n try {\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) return null;\n\n return (await res.json()) as OpenCollectiveProfile;\n } catch {\n return null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Repo slug resolution (shared helper)\n // ---------------------------------------------------------------------------\n\n private async resolveRepoSlug(\n packageName: string,\n ): Promise<{ owner: string; repo: string } | null> {\n try {\n const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) return null;\n\n const body = (await res.json()) as {\n repository?: { url?: string } | string;\n };\n\n const repoField = body.repository;\n const raw = typeof repoField === 'string' ? repoField : repoField?.url;\n if (!raw) return null;\n\n const normalised = raw\n .replace(/^git\\+/, '')\n .replace(/^git:\\/\\//, 'https://')\n .replace(/^ssh:\\/\\/git@github\\.com/, 'https://github.com')\n .replace(/^git@github\\.com:/, 'https://github.com/')\n .replace(/\\.git$/, '');\n\n const match = normalised.match(/github\\.com\\/([^/]+)\\/([^/]+)/);\n if (!match) return null;\n\n return { owner: match[1], repo: match[2] };\n } catch {\n return null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Utilities\n // ---------------------------------------------------------------------------\n\n private buildFundingUrls(\n npmFunding: string | string[] | null,\n fundingYml: string | null,\n oc: OpenCollectiveProfile | null,\n ): string[] {\n const urls: string[] = [];\n\n // From npm\n if (npmFunding) {\n if (typeof npmFunding === 'string') {\n urls.push(npmFunding);\n } else {\n urls.push(...npmFunding);\n }\n }\n\n // From FUNDING.yml -- parse known keys\n if (fundingYml) {\n const ghMatch = fundingYml.match(/github:\\s*(.+)/i);\n if (ghMatch) {\n const sponsors = ghMatch[1].trim()\n .replace(/^\\[/, '').replace(/]$/, '')\n .split(',')\n .map((s) => s.trim().replace(/['\"]/g, ''))\n .filter(Boolean);\n const GITHUB_USERNAME = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/;\n for (const sponsor of sponsors) {\n if (GITHUB_USERNAME.test(sponsor)) {\n urls.push(`https://github.com/sponsors/${sponsor}`);\n }\n }\n }\n\n const ocMatch = fundingYml.match(/open_collective:\\s*(\\S+)/i);\n if (ocMatch) {\n urls.push(`https://opencollective.com/${ocMatch[1].trim()}`);\n }\n\n const kofiMatch = fundingYml.match(/ko_fi:\\s*(\\S+)/i);\n if (kofiMatch) {\n urls.push(`https://ko-fi.com/${kofiMatch[1].trim()}`);\n }\n\n const patreonMatch = fundingYml.match(/patreon:\\s*(\\S+)/i);\n if (patreonMatch) {\n urls.push(`https://patreon.com/${patreonMatch[1].trim()}`);\n }\n }\n\n // OpenCollective\n if (oc?.slug) {\n const ocUrl = `https://opencollective.com/${oc.slug}`;\n if (!urls.includes(ocUrl)) {\n urls.push(ocUrl);\n }\n }\n\n return [...new Set(urls)];\n }\n}\n","/**\n * PopularityCollector -- measures package adoption through download counts\n * and trend analysis.\n *\n * Data sources:\n * - https://api.npmjs.org/downloads/point/last-week/{package}\n * - https://api.npmjs.org/downloads/point/last-month/{package}\n * - npm registry (dependent count, if available)\n */\n\nimport type { CollectorResult, PopularityData } from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\nimport { npmRateLimiter } from '../utils/rate-limiter.js';\nimport { BaseCollector } from './base.js';\n\ninterface NpmDownloadsResponse {\n downloads: number;\n start: string;\n end: string;\n package: string;\n}\n\ntype DownloadTrend = 'rising' | 'stable' | 'declining';\n\nexport class PopularityCollector extends BaseCollector<PopularityData> {\n readonly name = 'popularity';\n\n constructor(cache: CacheManager) {\n super(cache);\n }\n\n async collect(\n packageName: string,\n version: string,\n ): Promise<CollectorResult<PopularityData>> {\n // Check cache first\n const cached = await this.getCached(packageName, version);\n if (cached) return cached;\n\n try {\n const [weeklyDownloads, monthlyDownloads, dependentCount] =\n await Promise.all([\n this.fetchDownloads(packageName, 'last-week'),\n this.fetchDownloads(packageName, 'last-month'),\n this.fetchDependentCount(packageName),\n ]);\n\n // Trend calculation: compare weekly vs monthly weekly-average.\n const monthlyWeeklyAvg = monthlyDownloads / 4;\n let trend: DownloadTrend = 'stable';\n\n if (monthlyWeeklyAvg > 0) {\n const ratio = weeklyDownloads / monthlyWeeklyAvg;\n if (ratio > 1.1) {\n trend = 'rising';\n } else if (ratio < 0.9) {\n trend = 'declining';\n }\n }\n\n const data: PopularityData = {\n packageName,\n weeklyDownloads,\n monthlyDownloads,\n trend,\n dependentCount,\n };\n\n await this.setCache(packageName, version, data);\n\n return {\n status: 'success',\n data,\n collectedAt: new Date().toISOString(),\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`PopularityCollector failed for ${packageName}@${version}: ${message}`);\n\n return {\n status: 'error',\n data: null,\n error: message,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n // ---------------------------------------------------------------------------\n // npm downloads API\n // ---------------------------------------------------------------------------\n\n private async fetchDownloads(\n packageName: string,\n period: 'last-week' | 'last-month',\n ): Promise<number> {\n const url = `https://api.npmjs.org/downloads/point/${period}/${encodeURIComponent(packageName)}`;\n logger.debug(`Popularity: fetching ${period} downloads: ${url}`);\n\n try {\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) {\n logger.warn(`Downloads API returned ${res.status} for ${packageName} (${period})`);\n return 0;\n }\n\n const body = (await res.json()) as NpmDownloadsResponse;\n return body.downloads ?? 0;\n } catch {\n logger.warn(`Could not fetch ${period} downloads for ${packageName}`);\n return 0;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Dependent count\n // ---------------------------------------------------------------------------\n\n /**\n * Attempt to get the number of packages that depend on this one.\n *\n * The npm registry search API exposes a \"dependents\" count via the\n * /-/v1/search endpoint. This is an approximation.\n */\n private async fetchDependentCount(packageName: string): Promise<number> {\n const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(packageName)}&size=1`;\n logger.debug(`Popularity: fetching dependent count: ${url}`);\n\n try {\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) return 0;\n\n const body = (await res.json()) as {\n objects?: Array<{\n package: { name: string };\n searchScore?: number;\n // The npm search API does not directly expose dependent count,\n // but the registry CDN response sometimes does.\n }>;\n };\n\n // If the first result is an exact match we could use its score as a\n // proxy, but the number is not directly available. Return 0 for now\n // and rely on the registry packument approach below.\n if (body.objects && body.objects.length > 0) {\n const exactMatch = body.objects.find(\n (o) => o.package.name === packageName,\n );\n if (exactMatch) {\n // searchScore is not the dependent count, but we try a second\n // lookup from the packument \"users\" field (rarely populated).\n return await this.fetchDependentCountFromRegistry(packageName);\n }\n }\n\n return 0;\n } catch {\n return 0;\n }\n }\n\n /**\n * Secondary lookup: the npm registry exposes dependent info via the\n * abbreviated packument endpoint.\n */\n private async fetchDependentCountFromRegistry(\n packageName: string,\n ): Promise<number> {\n try {\n // The dependents count is not directly in the registry JSON.\n // We use the npm website API as a best-effort approach.\n const url = `https://www.npmjs.com/package/${encodeURIComponent(packageName)}`;\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: {\n Accept: 'text/html',\n 'X-Spiferack': '1', // npm returns JSON when this header is set\n },\n });\n\n if (!res.ok) return 0;\n\n const body = (await res.json()) as {\n dependents?: { dependentsCount?: number };\n };\n\n return body.dependents?.dependentsCount ?? 0;\n } catch {\n // This endpoint is undocumented and may change -- fail silently.\n return 0;\n }\n }\n}\n","/**\n * LicenseCollector -- determines the license of an npm package, maps it to\n * an SPDX identifier, and classifies its risk level.\n *\n * Risk classification:\n * safe - permissive licenses (MIT, ISC, BSD, Apache-2.0, etc.)\n * cautious - weak-copyleft licenses (LGPL, MPL, EPL)\n * risky - strong-copyleft licenses (GPL, AGPL)\n * unknown - unrecognised or missing license\n */\n\nimport type { CollectorResult, LicenseData } from '../parsers/schema.js';\nimport type { CacheManager } from '../cache/store.js';\nimport { logger } from '../utils/logger.js';\nimport { npmRateLimiter } from '../utils/rate-limiter.js';\nimport { BaseCollector } from './base.js';\n\ntype LicenseRisk = 'safe' | 'cautious' | 'risky' | 'unknown';\n\n/** Map of SPDX identifiers to their risk classification. */\nconst RISK_MAP: Record<string, LicenseRisk> = {\n // Safe -- permissive\n 'MIT': 'safe',\n 'ISC': 'safe',\n 'BSD-2-Clause': 'safe',\n 'BSD-3-Clause': 'safe',\n 'Apache-2.0': 'safe',\n 'Unlicense': 'safe',\n '0BSD': 'safe',\n 'CC0-1.0': 'safe',\n 'CC-BY-4.0': 'safe',\n 'Zlib': 'safe',\n 'BlueOak-1.0.0': 'safe',\n 'MIT-0': 'safe',\n\n // Cautious -- weak copyleft\n 'LGPL-2.1': 'cautious',\n 'LGPL-2.1-only': 'cautious',\n 'LGPL-2.1-or-later': 'cautious',\n 'LGPL-3.0': 'cautious',\n 'LGPL-3.0-only': 'cautious',\n 'LGPL-3.0-or-later': 'cautious',\n 'MPL-2.0': 'cautious',\n 'EPL-2.0': 'cautious',\n 'EPL-1.0': 'cautious',\n 'CDDL-1.0': 'cautious',\n 'CDDL-1.1': 'cautious',\n\n // Risky -- strong copyleft\n 'GPL-2.0': 'risky',\n 'GPL-2.0-only': 'risky',\n 'GPL-2.0-or-later': 'risky',\n 'GPL-3.0': 'risky',\n 'GPL-3.0-only': 'risky',\n 'GPL-3.0-or-later': 'risky',\n 'AGPL-3.0': 'risky',\n 'AGPL-3.0-only': 'risky',\n 'AGPL-3.0-or-later': 'risky',\n 'SSPL-1.0': 'risky',\n 'EUPL-1.2': 'risky',\n};\n\n/** Licenses known to be OSI-approved. */\nconst OSI_APPROVED = new Set<string>([\n 'MIT', 'ISC', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0',\n '0BSD', 'Unlicense',\n 'LGPL-2.1', 'LGPL-2.1-only', 'LGPL-2.1-or-later',\n 'LGPL-3.0', 'LGPL-3.0-only', 'LGPL-3.0-or-later',\n 'MPL-2.0', 'EPL-2.0', 'EPL-1.0',\n 'GPL-2.0', 'GPL-2.0-only', 'GPL-2.0-or-later',\n 'GPL-3.0', 'GPL-3.0-only', 'GPL-3.0-or-later',\n 'AGPL-3.0', 'AGPL-3.0-only', 'AGPL-3.0-or-later',\n 'CDDL-1.0', 'Artistic-2.0', 'Zlib', 'PostgreSQL',\n 'EUPL-1.2', 'ECL-2.0',\n]);\n\n/** Common non-SPDX license strings people put in package.json. */\nconst LICENSE_ALIASES: Record<string, string> = {\n 'apache 2.0': 'Apache-2.0',\n 'apache2': 'Apache-2.0',\n 'apache-2': 'Apache-2.0',\n 'apache license 2.0': 'Apache-2.0',\n 'bsd': 'BSD-2-Clause',\n 'bsd-2': 'BSD-2-Clause',\n 'bsd-3': 'BSD-3-Clause',\n 'bsd license': 'BSD-2-Clause',\n 'gpl': 'GPL-3.0',\n 'gpl-2': 'GPL-2.0',\n 'gpl-3': 'GPL-3.0',\n 'gplv2': 'GPL-2.0',\n 'gplv3': 'GPL-3.0',\n 'lgpl': 'LGPL-3.0',\n 'lgpl-2': 'LGPL-2.1',\n 'lgpl-3': 'LGPL-3.0',\n 'agpl': 'AGPL-3.0',\n 'agpl-3': 'AGPL-3.0',\n 'mpl': 'MPL-2.0',\n 'mpl-2': 'MPL-2.0',\n 'unlicensed': 'Unlicense',\n 'public domain': 'Unlicense',\n 'wtfpl': 'WTFPL',\n 'cc0': 'CC0-1.0',\n 'cc0-1.0': 'CC0-1.0',\n 'artistic-2.0': 'Artistic-2.0',\n};\n\nexport class LicenseCollector extends BaseCollector<LicenseData> {\n readonly name = 'license';\n\n constructor(cache: CacheManager) {\n super(cache);\n }\n\n async collect(\n packageName: string,\n version: string,\n ): Promise<CollectorResult<LicenseData>> {\n // Check cache first\n const cached = await this.getCached(packageName, version);\n if (cached) return cached;\n\n try {\n const rawLicense = await this.fetchLicense(packageName, version);\n const spdx = this.toSpdx(rawLicense);\n const risk = this.classifyRisk(spdx);\n const osiApproved = spdx !== null && OSI_APPROVED.has(spdx);\n\n const data: LicenseData = {\n packageName,\n version,\n raw: rawLicense,\n spdx,\n risk,\n osiApproved,\n };\n\n await this.setCache(packageName, version, data);\n\n return {\n status: 'success',\n data,\n collectedAt: new Date().toISOString(),\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`LicenseCollector failed for ${packageName}@${version}: ${message}`);\n\n return {\n status: 'error',\n data: null,\n error: message,\n collectedAt: new Date().toISOString(),\n };\n }\n }\n\n // ---------------------------------------------------------------------------\n // npm registry\n // ---------------------------------------------------------------------------\n\n /**\n * Fetch the license string from the npm registry.\n *\n * Tries the specific version first, then falls back to the top-level\n * packument \"license\" field.\n */\n private async fetchLicense(\n packageName: string,\n version: string,\n ): Promise<string | null> {\n const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;\n logger.debug(`License: fetching packument ${url}`);\n\n await npmRateLimiter.acquire();\n const res = await fetch(url, {\n headers: { Accept: 'application/json' },\n });\n\n if (!res.ok) {\n throw new Error(`npm registry returned ${res.status} for ${packageName}`);\n }\n\n const body = (await res.json()) as {\n license?: string | { type?: string };\n versions?: Record<string, { license?: string | { type?: string } }>;\n };\n\n // Try version-specific license first.\n const versionInfo = body.versions?.[version];\n if (versionInfo?.license) {\n return typeof versionInfo.license === 'string'\n ? versionInfo.license\n : versionInfo.license.type ?? null;\n }\n\n // Fallback to top-level license.\n if (body.license) {\n return typeof body.license === 'string'\n ? body.license\n : body.license.type ?? null;\n }\n\n return null;\n }\n\n // ---------------------------------------------------------------------------\n // SPDX mapping\n // ---------------------------------------------------------------------------\n\n /**\n * Normalise a raw license string to an SPDX identifier.\n *\n * Handles common aliases, SPDX expression syntax (e.g. \"(MIT OR Apache-2.0)\"),\n * and case-insensitive matching.\n */\n private toSpdx(raw: string | null): string | null {\n if (!raw) return null;\n\n const trimmed = raw.trim();\n if (!trimmed) return null;\n\n // If it is already a known SPDX id, return as-is.\n if (RISK_MAP[trimmed] !== undefined || OSI_APPROVED.has(trimmed)) {\n return trimmed;\n }\n\n // Try case-insensitive alias lookup.\n const lower = trimmed.toLowerCase();\n const alias = LICENSE_ALIASES[lower];\n if (alias) return alias;\n\n // Handle SPDX expressions like \"(MIT OR Apache-2.0)\".\n // We extract the first recognisable identifier for risk classification.\n const stripped = trimmed.replace(/[()]/g, '');\n const parts = stripped.split(/\\s+(?:OR|AND)\\s+/i);\n for (const part of parts) {\n const p = part.trim();\n if (RISK_MAP[p] !== undefined) return p;\n const pAlias = LICENSE_ALIASES[p.toLowerCase()];\n if (pAlias) return pAlias;\n }\n\n // Return the original trimmed value (may be a valid but uncommon SPDX id).\n return trimmed;\n }\n\n // ---------------------------------------------------------------------------\n // Risk classification\n // ---------------------------------------------------------------------------\n\n private classifyRisk(spdx: string | null): LicenseRisk {\n if (!spdx) return 'unknown';\n return RISK_MAP[spdx] ?? 'unknown';\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BaseParser,
|
|
4
|
+
createDependencyNode,
|
|
5
|
+
createDependencyTree
|
|
6
|
+
} from "./chunk-UMB5MJHL.js";
|
|
7
|
+
|
|
8
|
+
// src/parsers/npm.ts
|
|
9
|
+
import { readFile, access } from "fs/promises";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
async function fileExists(path) {
|
|
12
|
+
try {
|
|
13
|
+
await access(path);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function readJson(path) {
|
|
20
|
+
const raw = await readFile(path, "utf-8");
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
async function readText(path) {
|
|
24
|
+
return readFile(path, "utf-8");
|
|
25
|
+
}
|
|
26
|
+
function addDirectDeps(tree, deps) {
|
|
27
|
+
if (!deps) return;
|
|
28
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
29
|
+
const key = `${name}@${version}`;
|
|
30
|
+
if (tree.nodes.has(key)) continue;
|
|
31
|
+
tree.nodes.set(
|
|
32
|
+
key,
|
|
33
|
+
createDependencyNode({ name, version, registry: "npm", depth: 0, isDirect: true, parent: null })
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parsePackageLock(lockData, directNames, tree) {
|
|
38
|
+
const packages = lockData.packages ?? {};
|
|
39
|
+
for (const [pkgPath, meta] of Object.entries(packages)) {
|
|
40
|
+
if (pkgPath === "") continue;
|
|
41
|
+
const segments = pkgPath.split("node_modules/");
|
|
42
|
+
const name = segments[segments.length - 1];
|
|
43
|
+
if (!name) continue;
|
|
44
|
+
const version = meta.version ?? "unknown";
|
|
45
|
+
const depth = segments.length - 1;
|
|
46
|
+
const isDirect = depth === 1 && directNames.has(name);
|
|
47
|
+
const key = `${name}@${version}`;
|
|
48
|
+
if (tree.nodes.has(key)) continue;
|
|
49
|
+
tree.nodes.set(
|
|
50
|
+
key,
|
|
51
|
+
createDependencyNode({
|
|
52
|
+
name,
|
|
53
|
+
version,
|
|
54
|
+
registry: "npm",
|
|
55
|
+
depth: isDirect ? 0 : depth,
|
|
56
|
+
isDirect,
|
|
57
|
+
parent: isDirect ? null : inferParent(pkgPath)
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function inferParent(pkgPath) {
|
|
63
|
+
const parts = pkgPath.split("/node_modules/");
|
|
64
|
+
if (parts.length < 2) return null;
|
|
65
|
+
return parts.length >= 2 ? parts[parts.length - 2] : null;
|
|
66
|
+
}
|
|
67
|
+
function parseYarnLock(content, directNames, tree) {
|
|
68
|
+
const lines = content.split("\n");
|
|
69
|
+
let currentNames = [];
|
|
70
|
+
let currentVersion = null;
|
|
71
|
+
const flush = () => {
|
|
72
|
+
if (currentNames.length === 0 || !currentVersion) return;
|
|
73
|
+
for (const rawName of currentNames) {
|
|
74
|
+
const atIdx = rawName.lastIndexOf("@");
|
|
75
|
+
const name = atIdx > 0 ? rawName.slice(0, atIdx) : rawName;
|
|
76
|
+
const version = currentVersion;
|
|
77
|
+
const isDirect = directNames.has(name);
|
|
78
|
+
const key = `${name}@${version}`;
|
|
79
|
+
if (tree.nodes.has(key)) continue;
|
|
80
|
+
tree.nodes.set(
|
|
81
|
+
key,
|
|
82
|
+
createDependencyNode({
|
|
83
|
+
name,
|
|
84
|
+
version,
|
|
85
|
+
registry: "npm",
|
|
86
|
+
depth: isDirect ? 0 : 1,
|
|
87
|
+
isDirect,
|
|
88
|
+
parent: null
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
95
|
+
if (!line.startsWith(" ") && !line.startsWith(" ")) {
|
|
96
|
+
flush();
|
|
97
|
+
currentNames = [];
|
|
98
|
+
currentVersion = null;
|
|
99
|
+
const cleaned = line.replace(/:$/, "").trim();
|
|
100
|
+
const entries = cleaned.split(",").map((s) => s.trim().replace(/^"|"$/g, ""));
|
|
101
|
+
currentNames = entries.filter(Boolean);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const versionMatch = line.match(/^\s+version\s+"?([^"]+)"?/);
|
|
105
|
+
if (versionMatch) {
|
|
106
|
+
currentVersion = versionMatch[1];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
flush();
|
|
110
|
+
}
|
|
111
|
+
function parsePnpmLock(content, directNames, tree) {
|
|
112
|
+
const lines = content.split("\n");
|
|
113
|
+
let inPackages = false;
|
|
114
|
+
let currentPackageName = null;
|
|
115
|
+
let inDependencies = false;
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
const trimmed = line.trimEnd();
|
|
118
|
+
if (/^packages:/.test(trimmed)) {
|
|
119
|
+
inPackages = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (inPackages && /^\S/.test(trimmed) && !trimmed.startsWith(" ") && !trimmed.startsWith("'") && !trimmed.startsWith("/")) {
|
|
123
|
+
if (!trimmed.includes("@") && trimmed.endsWith(":")) {
|
|
124
|
+
inPackages = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!inPackages) continue;
|
|
129
|
+
const pkgMatch = trimmed.match(
|
|
130
|
+
/^\s{2,4}(?:'|")?\/?((?:@[^/@]+\/)?[^@:'"]+)@([^:'"]+)(?:'|")?:\s*$/
|
|
131
|
+
);
|
|
132
|
+
if (pkgMatch) {
|
|
133
|
+
const [, name, version] = pkgMatch;
|
|
134
|
+
if (!name || !version) continue;
|
|
135
|
+
currentPackageName = name;
|
|
136
|
+
inDependencies = false;
|
|
137
|
+
const isDirect = directNames.has(name);
|
|
138
|
+
const key = `${name}@${version}`;
|
|
139
|
+
if (tree.nodes.has(key)) continue;
|
|
140
|
+
tree.nodes.set(
|
|
141
|
+
key,
|
|
142
|
+
createDependencyNode({
|
|
143
|
+
name,
|
|
144
|
+
version,
|
|
145
|
+
registry: "npm",
|
|
146
|
+
depth: isDirect ? 0 : 1,
|
|
147
|
+
isDirect,
|
|
148
|
+
parent: null
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (/^\s{4,6}dependencies:/.test(trimmed)) {
|
|
154
|
+
inDependencies = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (inDependencies && /^\s{4,6}\S/.test(trimmed) && !trimmed.match(/^\s{6,}/)) {
|
|
158
|
+
inDependencies = false;
|
|
159
|
+
}
|
|
160
|
+
if (inDependencies && currentPackageName) {
|
|
161
|
+
const depMatch = trimmed.match(/^\s{6,8}(?:'|")?([^:'"]+)(?:'|")?\s*:\s*(?:'|")?([^'"]+)(?:'|")?/);
|
|
162
|
+
if (depMatch) {
|
|
163
|
+
const [, depName, depVersion] = depMatch;
|
|
164
|
+
if (!depName || !depVersion) continue;
|
|
165
|
+
const cleanVersion = depVersion.trim();
|
|
166
|
+
const isDirect = directNames.has(depName);
|
|
167
|
+
const key = `${depName}@${cleanVersion}`;
|
|
168
|
+
if (tree.nodes.has(key)) continue;
|
|
169
|
+
tree.nodes.set(
|
|
170
|
+
key,
|
|
171
|
+
createDependencyNode({
|
|
172
|
+
name: depName,
|
|
173
|
+
version: cleanVersion,
|
|
174
|
+
registry: "npm",
|
|
175
|
+
depth: isDirect ? 0 : 2,
|
|
176
|
+
isDirect,
|
|
177
|
+
parent: isDirect ? null : currentPackageName
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
var NpmParser = class extends BaseParser {
|
|
185
|
+
name = "npm";
|
|
186
|
+
async detect(dir) {
|
|
187
|
+
return fileExists(join(dir, "package.json"));
|
|
188
|
+
}
|
|
189
|
+
async parse(dir) {
|
|
190
|
+
const pkgPath = join(dir, "package.json");
|
|
191
|
+
const pkg = await readJson(pkgPath);
|
|
192
|
+
const projectName = pkg.name ?? "unknown";
|
|
193
|
+
const tree = createDependencyTree(projectName, pkgPath);
|
|
194
|
+
const allDirect = {
|
|
195
|
+
...pkg.dependencies,
|
|
196
|
+
...pkg.devDependencies
|
|
197
|
+
};
|
|
198
|
+
const directNames = new Set(Object.keys(allDirect));
|
|
199
|
+
addDirectDeps(tree, pkg.dependencies);
|
|
200
|
+
addDirectDeps(tree, pkg.devDependencies);
|
|
201
|
+
const lockPath = join(dir, "package-lock.json");
|
|
202
|
+
const yarnPath = join(dir, "yarn.lock");
|
|
203
|
+
const pnpmPath = join(dir, "pnpm-lock.yaml");
|
|
204
|
+
if (await fileExists(lockPath)) {
|
|
205
|
+
const lockData = await readJson(lockPath);
|
|
206
|
+
parsePackageLock(lockData, directNames, tree);
|
|
207
|
+
} else if (await fileExists(yarnPath)) {
|
|
208
|
+
const yarnContent = await readText(yarnPath);
|
|
209
|
+
parseYarnLock(yarnContent, directNames, tree);
|
|
210
|
+
} else if (await fileExists(pnpmPath)) {
|
|
211
|
+
const pnpmContent = await readText(pnpmPath);
|
|
212
|
+
parsePnpmLock(pnpmContent, directNames, tree);
|
|
213
|
+
}
|
|
214
|
+
this.deduplicateDirectDeps(tree);
|
|
215
|
+
tree.totalDirect = 0;
|
|
216
|
+
tree.totalTransitive = 0;
|
|
217
|
+
for (const node of tree.nodes.values()) {
|
|
218
|
+
if (node.isDirect) {
|
|
219
|
+
tree.totalDirect++;
|
|
220
|
+
} else {
|
|
221
|
+
tree.totalTransitive++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return tree;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* When both a version-range entry (from package.json) and a resolved-version
|
|
228
|
+
* entry (from lockfile) exist for the same package name, keep only the
|
|
229
|
+
* resolved one.
|
|
230
|
+
*
|
|
231
|
+
* A version is considered "resolved" when it does NOT start with ^, ~, >=, etc.
|
|
232
|
+
*/
|
|
233
|
+
deduplicateDirectDeps(tree) {
|
|
234
|
+
const byName = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const [key, node] of tree.nodes.entries()) {
|
|
236
|
+
if (!node.isDirect) continue;
|
|
237
|
+
const list = byName.get(node.name) ?? [];
|
|
238
|
+
list.push({ key, version: node.version });
|
|
239
|
+
byName.set(node.name, list);
|
|
240
|
+
}
|
|
241
|
+
for (const [, entries] of byName) {
|
|
242
|
+
if (entries.length <= 1) continue;
|
|
243
|
+
const isRange = (v) => /^[~^>=<*]/.test(v) || v === "latest";
|
|
244
|
+
const resolved = entries.filter((e) => !isRange(e.version));
|
|
245
|
+
const ranges = entries.filter((e) => isRange(e.version));
|
|
246
|
+
if (resolved.length > 0 && ranges.length > 0) {
|
|
247
|
+
for (const range of ranges) {
|
|
248
|
+
tree.nodes.delete(range.key);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export {
|
|
256
|
+
NpmParser
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=chunk-7DST6SNA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parsers/npm.ts"],"sourcesContent":["import { readFile, access } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { BaseParser } from \"./base.js\";\nimport {\n type DependencyTree,\n createDependencyTree,\n createDependencyNode,\n} from \"./schema.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function readJson<T = unknown>(path: string): Promise<T> {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n}\n\nasync function readText(path: string): Promise<string> {\n return readFile(path, \"utf-8\");\n}\n\n/** Merge a record of { name: versionSpec } into the nodes map. */\nfunction addDirectDeps(\n tree: DependencyTree,\n deps: Record<string, string> | undefined,\n): void {\n if (!deps) return;\n for (const [name, version] of Object.entries(deps)) {\n const key = `${name}@${version}`;\n if (tree.nodes.has(key)) continue;\n tree.nodes.set(\n key,\n createDependencyNode({ name, version, registry: \"npm\", depth: 0, isDirect: true, parent: null }),\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Lock-file parsers\n// ---------------------------------------------------------------------------\n\n/**\n * Parse package-lock.json v2/v3 \"packages\" field.\n *\n * The top-level key \"\" is the project root — skip it. Every other key is of\n * the form \"node_modules/<name>\" (possibly nested).\n */\nfunction parsePackageLock(\n lockData: PackageLockJson,\n directNames: Set<string>,\n tree: DependencyTree,\n): void {\n const packages = lockData.packages ?? {};\n\n for (const [pkgPath, meta] of Object.entries(packages)) {\n // Skip the root entry\n if (pkgPath === \"\") continue;\n\n // Extract the package name from the path.\n // e.g. \"node_modules/express\" -> \"express\"\n // \"node_modules/@scope/pkg\" -> \"@scope/pkg\"\n // \"node_modules/a/node_modules/b\" -> \"b\"\n const segments = pkgPath.split(\"node_modules/\");\n const name = segments[segments.length - 1];\n if (!name) continue;\n\n const version = meta.version ?? \"unknown\";\n const depth = segments.length - 1; // 1 = direct level, 2+ = transitive\n const isDirect = depth === 1 && directNames.has(name);\n\n const key = `${name}@${version}`;\n if (tree.nodes.has(key)) continue;\n\n tree.nodes.set(\n key,\n createDependencyNode({\n name,\n version,\n registry: \"npm\",\n depth: isDirect ? 0 : depth,\n isDirect,\n parent: isDirect ? null : inferParent(pkgPath),\n }),\n );\n }\n}\n\n/** Extract the parent package name from a nested node_modules path. */\nfunction inferParent(pkgPath: string): string | null {\n // \"node_modules/a/node_modules/b\" -> parent is \"a\"\n const parts = pkgPath.split(\"/node_modules/\");\n if (parts.length < 2) return null;\n // The second-to-last segment is the parent\n return parts.length >= 2 ? parts[parts.length - 2] : null;\n}\n\n/**\n * Parse a yarn.lock (v1) flat format.\n *\n * Each block looks like:\n * ```\n * \"express@^4.18.0\":\n * version \"4.18.2\"\n * resolved \"https://...\"\n * ...\n * ```\n */\nfunction parseYarnLock(\n content: string,\n directNames: Set<string>,\n tree: DependencyTree,\n): void {\n const lines = content.split(\"\\n\");\n let currentNames: string[] = [];\n let currentVersion: string | null = null;\n\n const flush = (): void => {\n if (currentNames.length === 0 || !currentVersion) return;\n for (const rawName of currentNames) {\n // rawName is e.g. \"express@^4.18.0\" or \"@scope/pkg@~1.0.0\"\n const atIdx = rawName.lastIndexOf(\"@\");\n const name = atIdx > 0 ? rawName.slice(0, atIdx) : rawName;\n const version = currentVersion;\n const isDirect = directNames.has(name);\n const key = `${name}@${version}`;\n if (tree.nodes.has(key)) continue;\n\n tree.nodes.set(\n key,\n createDependencyNode({\n name,\n version,\n registry: \"npm\",\n depth: isDirect ? 0 : 1,\n isDirect,\n parent: null,\n }),\n );\n }\n };\n\n for (const line of lines) {\n // Skip comments and blank lines\n if (line.startsWith(\"#\") || line.trim() === \"\") continue;\n\n // Header line (no leading whitespace)\n if (!line.startsWith(\" \") && !line.startsWith(\"\\t\")) {\n flush();\n currentNames = [];\n currentVersion = null;\n\n // Parse the header: could have multiple comma-separated entries\n // e.g.: \"chalk@^4.0.0\", \"chalk@^4.1.0\":\n const cleaned = line.replace(/:$/, \"\").trim();\n const entries = cleaned.split(\",\").map((s) => s.trim().replace(/^\"|\"$/g, \"\"));\n currentNames = entries.filter(Boolean);\n continue;\n }\n\n // version field inside a block\n const versionMatch = line.match(/^\\s+version\\s+\"?([^\"]+)\"?/);\n if (versionMatch) {\n currentVersion = versionMatch[1];\n }\n }\n // Flush the last block\n flush();\n}\n\n/**\n * Parse pnpm-lock.yaml (lockfileVersion 9 format).\n *\n * The interesting section is `packages:` which is a map like:\n * /@scope/pkg@1.2.3:\n * resolution: ...\n * dependencies: ...\n *\n * In newer v9 the key format is: `<name>@<version>` (no leading slash).\n *\n * We use a lightweight regex approach to avoid pulling in a YAML parser as a\n * hard dependency — this project already has zero YAML dependencies.\n */\nfunction parsePnpmLock(\n content: string,\n directNames: Set<string>,\n tree: DependencyTree,\n): void {\n // Match lines that look like package entries under the `packages:` section.\n // Both formats:\n // /@scope/name@1.0.0: (lockfileVersion < 9)\n // @scope/name@1.0.0: (lockfileVersion 9)\n // name@1.0.0: (unscoped)\n // /name@1.0.0: (unscoped, old)\n // Lines inside a `packages:` block that start at column 2+ with an @ or letter.\n\n const lines = content.split(\"\\n\");\n let inPackages = false;\n // Track the current package context for nested dependency parsing\n let currentPackageName: string | null = null;\n let inDependencies = false;\n\n for (const line of lines) {\n const trimmed = line.trimEnd();\n\n // Detect the packages section\n if (/^packages:/.test(trimmed)) {\n inPackages = true;\n continue;\n }\n\n // If we hit another top-level key, stop\n if (inPackages && /^\\S/.test(trimmed) && !trimmed.startsWith(\" \") && !trimmed.startsWith(\"'\") && !trimmed.startsWith(\"/\")) {\n // Could be another top-level key like \"snapshots:\" or \"importers:\"\n if (!trimmed.includes(\"@\") && trimmed.endsWith(\":\")) {\n inPackages = false;\n continue;\n }\n }\n\n if (!inPackages) continue;\n\n // Package entry line (indented with 2 spaces or starts with / or quoted)\n // Patterns:\n // ' /@scope/pkg@1.0.0:'\n // ' name@1.0.0:'\n // ' @scope/name@1.0.0:'\n const pkgMatch = trimmed.match(\n /^\\s{2,4}(?:'|\")?\\/?((?:@[^/@]+\\/)?[^@:'\"]+)@([^:'\"]+)(?:'|\")?:\\s*$/,\n );\n\n if (pkgMatch) {\n const [, name, version] = pkgMatch;\n if (!name || !version) continue;\n\n currentPackageName = name;\n inDependencies = false;\n\n const isDirect = directNames.has(name);\n const key = `${name}@${version}`;\n if (tree.nodes.has(key)) continue;\n\n tree.nodes.set(\n key,\n createDependencyNode({\n name,\n version,\n registry: \"npm\",\n depth: isDirect ? 0 : 1,\n isDirect,\n parent: null,\n }),\n );\n continue;\n }\n\n // Detect \"dependencies:\" sub-block\n if (/^\\s{4,6}dependencies:/.test(trimmed)) {\n inDependencies = true;\n continue;\n }\n\n // Detect end of dependencies sub-block (less indentation or new section)\n if (inDependencies && /^\\s{4,6}\\S/.test(trimmed) && !trimmed.match(/^\\s{6,}/)) {\n inDependencies = false;\n }\n\n // Parse individual dependency lines inside a dependencies block\n if (inDependencies && currentPackageName) {\n const depMatch = trimmed.match(/^\\s{6,8}(?:'|\")?([^:'\"]+)(?:'|\")?\\s*:\\s*(?:'|\")?([^'\"]+)(?:'|\")?/);\n if (depMatch) {\n const [, depName, depVersion] = depMatch;\n if (!depName || !depVersion) continue;\n\n const cleanVersion = depVersion.trim();\n const isDirect = directNames.has(depName);\n const key = `${depName}@${cleanVersion}`;\n if (tree.nodes.has(key)) continue;\n\n tree.nodes.set(\n key,\n createDependencyNode({\n name: depName,\n version: cleanVersion,\n registry: \"npm\",\n depth: isDirect ? 0 : 2,\n isDirect,\n parent: isDirect ? null : currentPackageName,\n }),\n );\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types for package-lock.json\n// ---------------------------------------------------------------------------\n\ninterface PackageLockJson {\n lockfileVersion?: number;\n packages?: Record<string, { version?: string; dependencies?: Record<string, string> }>;\n}\n\ninterface PackageJson {\n name?: string;\n version?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n optionalDependencies?: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// NpmParser\n// ---------------------------------------------------------------------------\n\nexport class NpmParser extends BaseParser {\n readonly name = \"npm\";\n\n async detect(dir: string): Promise<boolean> {\n return fileExists(join(dir, \"package.json\"));\n }\n\n async parse(dir: string): Promise<DependencyTree> {\n const pkgPath = join(dir, \"package.json\");\n const pkg = await readJson<PackageJson>(pkgPath);\n\n const projectName = pkg.name ?? \"unknown\";\n const tree = createDependencyTree(projectName, pkgPath);\n\n // Collect direct dependency names for reference during lock-file parsing\n const allDirect: Record<string, string> = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n };\n const directNames = new Set(Object.keys(allDirect));\n\n // Add direct dependencies first\n addDirectDeps(tree, pkg.dependencies);\n addDirectDeps(tree, pkg.devDependencies);\n\n // Try lock files in priority order: package-lock.json > yarn.lock > pnpm-lock.yaml\n const lockPath = join(dir, \"package-lock.json\");\n const yarnPath = join(dir, \"yarn.lock\");\n const pnpmPath = join(dir, \"pnpm-lock.yaml\");\n\n if (await fileExists(lockPath)) {\n const lockData = await readJson<PackageLockJson>(lockPath);\n parsePackageLock(lockData, directNames, tree);\n } else if (await fileExists(yarnPath)) {\n const yarnContent = await readText(yarnPath);\n parseYarnLock(yarnContent, directNames, tree);\n } else if (await fileExists(pnpmPath)) {\n const pnpmContent = await readText(pnpmPath);\n parsePnpmLock(pnpmContent, directNames, tree);\n }\n // If no lock file is found, we already have direct deps from package.json\n\n // Deduplicate direct deps: when a lockfile provided resolved versions,\n // remove the version-range entries (e.g. keep \"chalk@5.6.2\", remove \"chalk@^5.3.0\").\n this.deduplicateDirectDeps(tree);\n\n // Recount totals — lock file parsing may have adjusted depths\n tree.totalDirect = 0;\n tree.totalTransitive = 0;\n for (const node of tree.nodes.values()) {\n if (node.isDirect) {\n tree.totalDirect++;\n } else {\n tree.totalTransitive++;\n }\n }\n\n return tree;\n }\n\n /**\n * When both a version-range entry (from package.json) and a resolved-version\n * entry (from lockfile) exist for the same package name, keep only the\n * resolved one.\n *\n * A version is considered \"resolved\" when it does NOT start with ^, ~, >=, etc.\n */\n private deduplicateDirectDeps(tree: DependencyTree): void {\n // Group direct deps by package name\n const byName = new Map<string, Array<{ key: string; version: string }>>();\n\n for (const [key, node] of tree.nodes.entries()) {\n if (!node.isDirect) continue;\n const list = byName.get(node.name) ?? [];\n list.push({ key, version: node.version });\n byName.set(node.name, list);\n }\n\n for (const [, entries] of byName) {\n if (entries.length <= 1) continue;\n\n // Determine which entries are version ranges\n const isRange = (v: string) => /^[~^>=<*]/.test(v) || v === 'latest';\n const resolved = entries.filter((e) => !isRange(e.version));\n const ranges = entries.filter((e) => isRange(e.version));\n\n // If we have at least one resolved version, remove all range entries\n if (resolved.length > 0 && ranges.length > 0) {\n for (const range of ranges) {\n tree.nodes.delete(range.key);\n }\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
|