Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default [
globals: {
console: 'readonly',
process: 'readonly',
URL: 'readonly',
},
},
rules: {
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,6 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"eslint": "^10.5.0",
"prettier": "^3.8.4"
"prettier": "^3.9.6"
}
}
93 changes: 13 additions & 80 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 2 additions & 10 deletions src/scanners/apt.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
})
}
80 changes: 10 additions & 70 deletions src/scanners/dnf.js
Original file line number Diff line number Diff line change
@@ -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,
})
}
13 changes: 13 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down