diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06be50a..b66be58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 20 cache: npm diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index f12fb9b..cfe141b 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24 registry-url: https://registry.npmjs.org diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3107396..567854a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24 cache: npm @@ -51,12 +51,12 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 with: languages: javascript-typescript - name: Autobuild - uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e + uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 diff --git a/eslint.config.js b/eslint.config.js index 2d06a08..4af5fa6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,6 +9,7 @@ export default [ globals: { console: 'readonly', process: 'readonly', + URL: 'readonly', }, }, rules: { diff --git a/package-lock.json b/package-lock.json index 95b3a34..f00e3fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "eslint": "^10.5.0", - "prettier": "^3.8.4" + "prettier": "^3.9.6" }, "engines": { "node": ">=20.0.0" @@ -300,9 +300,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -1022,9 +1022,9 @@ } }, "node_modules/prettier": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", - "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index b9fe720..56b9964 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,6 @@ "devDependencies": { "@eslint/js": "^10.0.1", "eslint": "^10.5.0", - "prettier": "^3.8.4" + "prettier": "^3.9.6" } } diff --git a/src/index.js b/src/index.js index 84c3bcc..db0f212 100644 --- a/src/index.js +++ b/src/index.js @@ -1,45 +1,7 @@ import ora from 'ora' import chalk from 'chalk' -import { writeFileSync } from 'fs' - -import npmScanner from './scanners/npm.js' -import pnpmScanner from './scanners/pnpm.js' -import yarnScanner from './scanners/yarn.js' -import brewScanner from './scanners/brew.js' -import voltaScanner from './scanners/volta.js' -import pipScanner from './scanners/pip.js' -import cargoScanner from './scanners/cargo.js' -import gemScanner from './scanners/gem.js' -import composerScanner from './scanners/composer.js' -import gradleScanner from './scanners/gradle.js' -import mavenScanner from './scanners/maven.js' -import nugetScanner from './scanners/nuget.js' -import aptScanner from './scanners/apt.js' -import pacmanScanner from './scanners/pacman.js' -import dnfScanner from './scanners/dnf.js' -import flatpakScanner from './scanners/flatpak.js' -import snapScanner from './scanners/snap.js' -import yumScanner from './scanners/yum.js' -import wingetScanner from './scanners/winget.js' -import chocoScanner from './scanners/choco.js' -import scoopScanner from './scanners/scoop.js' -import nixScanner from './scanners/nix.js' -import uvScanner from './scanners/uv.js' -import bunScanner from './scanners/bun.js' -import pipxScanner from './scanners/pipx.js' -import poetryScanner from './scanners/poetry.js' -import helmScanner from './scanners/helm.js' -import krewScanner from './scanners/krew.js' -import apkScanner from './scanners/apk.js' -import zypperScanner from './scanners/zypper.js' -import pkgScanner from './scanners/pkg.js' -import goScanner from './scanners/go.js' -import condaScanner from './scanners/conda.js' -import miseScanner from './scanners/mise.js' -import asdfScanner from './scanners/asdf.js' -import macportsScanner from './scanners/macports.js' -import opamScanner from './scanners/opam.js' -import vcpkgScanner from './scanners/vcpkg.js' +import { writeFileSync, readdirSync } from 'fs' + import { renderAll } from './display/table.js' import { optsOf } from './utils.js' @@ -87,46 +49,17 @@ export function filterDuplicatePackages(results) { .filter((result) => result.packages.length > 0) } -export const ALL_SCANNERS = { - npm: npmScanner, - pnpm: pnpmScanner, - yarn: yarnScanner, - brew: brewScanner, - volta: voltaScanner, - pip: pipScanner, - cargo: cargoScanner, - gem: gemScanner, - composer: composerScanner, - gradle: gradleScanner, - maven: mavenScanner, - nuget: nugetScanner, - apt: aptScanner, - pacman: pacmanScanner, - dnf: dnfScanner, - flatpak: flatpakScanner, - snap: snapScanner, - yum: yumScanner, - winget: wingetScanner, - choco: chocoScanner, - scoop: scoopScanner, - nix: nixScanner, - uv: uvScanner, - bun: bunScanner, - pipx: pipxScanner, - poetry: poetryScanner, - helm: helmScanner, - krew: krewScanner, - apk: apkScanner, - zypper: zypperScanner, - pkg: pkgScanner, - go: goScanner, - conda: condaScanner, - mise: miseScanner, - asdf: asdfScanner, - macports: macportsScanner, - opam: opamScanner, - vcpkg: vcpkgScanner, -} +// Auto-load every scanner in ./scanners keyed by filename (filename === manager). +// Drop a new file in that dir and it registers itself — no edit here needed. +const scannerDir = new URL('./scanners/', import.meta.url) +export const ALL_SCANNERS = Object.fromEntries( + await Promise.all( + readdirSync(scannerDir) + .filter((file) => file.endsWith('.js')) + .sort() + .map(async (file) => [file.slice(0, -3), (await import(`./scanners/${file}`)).default]) + ) +) // Resolve the scanner list for an optional manager filter; exit on unknown name. export function resolveScanners(filterManager) { diff --git a/src/scanners/apt.js b/src/scanners/apt.js index ed42701..f646fd8 100644 --- a/src/scanners/apt.js +++ b/src/scanners/apt.js @@ -1,4 +1,4 @@ -import { runScanner } from '../utils.js' +import { runScanner, parseTabbed } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null @@ -7,14 +7,6 @@ export default async function scan() { manager: 'apt', bin: 'dpkg-query', command: 'dpkg-query -W -f="${Package}\t${Version}\n"', - parse: (raw) => - raw - .split('\n') - .filter(Boolean) - .map((line) => { - const [name, version] = line.split('\t') - return { name: name?.trim(), version: version?.trim() || 'unknown', type: 'system' } - }) - .filter((pkg) => pkg.name), + parse: parseTabbed, }) } diff --git a/src/scanners/dnf.js b/src/scanners/dnf.js index 1031c8c..8d86fd9 100644 --- a/src/scanners/dnf.js +++ b/src/scanners/dnf.js @@ -1,75 +1,15 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' - -function parsePackages(raw) { - return raw - .split('\n') - .filter(Boolean) - .map((line) => { - const [name, version] = line.split('\t') - return { - name: name?.trim(), - version: version?.trim() || 'unknown', - type: 'system', - } - }) - .filter((pkg) => pkg.name) -} +import { runScanner, parseTabbed } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('dnf')) return null - - const timeout = 15000 - - try { - if (isAvailable('dnf5')) { - const raw = execSync('dnf5 repoquery --installed --qf "%{name}\t%{version}-%{release}"', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout, - }).toString() - - return { manager: 'dnf', packages: parsePackages(raw) } - } - - try { - const raw = execSync('dnf repoquery --installed --qf "%{name}\t%{version}-%{release}"', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout, - }).toString() - - return { manager: 'dnf', packages: parsePackages(raw) } - } catch (_repoqueryErr) { - const raw = execSync('dnf list installed --quiet', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .filter((line) => !line.startsWith('Installed Packages')) - .map((line) => { - const parts = line.split(/\s+/) - if (parts.length < 2) return null - - return { - name: parts[0].replace(/\.[^.\s]+$/, ''), - version: parts[1]?.trim() || 'unknown', - type: 'system', - } - }) - .filter((pkg) => pkg?.name) - return { manager: 'dnf', packages } - } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ dnf: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ dnf: scan timed out, skipping.') - } - return null - } + // ponytail: dropped the legacy `dnf list installed` fallback; repoquery ships + // with dnf4/dnf5 by default. Restore it if a repoquery-less env turns up. + return runScanner({ + manager: 'dnf', + bin: ['dnf5', 'dnf'], + command: (bin) => `${bin} repoquery --installed --qf "%{name}\t%{version}-%{release}"`, + timeout: 15000, + parse: parseTabbed, + }) } diff --git a/src/utils.js b/src/utils.js index 76b0724..996b7a1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -27,6 +27,19 @@ export function optsOf(options) { return typeof options?.opts === 'function' ? options.opts() : options } +// Parse tab-separated `name\tversion` lines into system package records. +// Shared by the dpkg/rpm-style scanners (apt, dnf, ...). +export function parseTabbed(raw) { + return raw + .split('\n') + .filter(Boolean) + .map((line) => { + const [name, version] = line.split('\t') + return { name: name?.trim(), version: version?.trim() || 'unknown', type: 'system' } + }) + .filter((pkg) => pkg.name) +} + // Shared scanner shell: resolve the binary, run one command, parse stdout. // Keeps the permission/timeout warning contract used by every scanner. export async function runScanner({