From 29be7968cd109af8fe36b418a59d292fcb153533 Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:03:22 +0700 Subject: [PATCH 01/10] chore(deps-dev): bump eslint from 10.4.1 to 10.5.0 --- package-lock.json | 11 +++++++---- package.json | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 922f608..6f9d6d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "eslint": "^10.4.0", + "eslint": "^10.5.0", "prettier": "^3.0.0" }, "engines": { @@ -435,11 +435,14 @@ } }, "node_modules/eslint": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", - "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", diff --git a/package.json b/package.json index 13606ac..f7d39b8 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "eslint": "^10.4.0", + "eslint": "^10.5.0", "prettier": "^3.0.0" } } From 41f9f3ae4c956f09efc2ffc3158b007081ca0feb Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:03:28 +0700 Subject: [PATCH 02/10] chore(deps-dev): bump prettier from 3.8.3 to 3.8.4 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f9d6d7..87be985 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.0.0" + "prettier": "^3.8.4" }, "engines": { "node": ">=20.0.0" @@ -1022,9 +1022,9 @@ } }, "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index f7d39b8..4d089c8 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,6 @@ "devDependencies": { "@eslint/js": "^10.0.1", "eslint": "^10.5.0", - "prettier": "^3.0.0" + "prettier": "^3.8.4" } } From 5685a9e4f04ce87959c8004c57a0bc853e6e3969 Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:03:42 +0700 Subject: [PATCH 03/10] refactor: add runScanner helper for shared scanner boilerplate --- src/utils.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/utils.js b/src/utils.js index 601b98b..25f3df9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,3 +9,30 @@ export function isAvailable(cmd) { return false } } + +// 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({ + manager, + bin, + command, + parse, + permissionHint, + timeout = 10000, +}) { + const binName = Array.isArray(bin) ? bin.find(isAvailable) : isAvailable(bin) ? bin : null + if (!binName) return null + + try { + const cmd = typeof command === 'function' ? command(binName) : command + const raw = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'], timeout }).toString() + return { manager, packages: parse(raw) } + } catch (err) { + if (err.message?.includes('EACCES') || err.message?.includes('permission')) { + console.warn(`⚠ ${manager}: ${permissionHint || 'permission denied. Try running with sudo.'}`) + } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { + console.warn(`⚠ ${manager}: scan timed out, skipping.`) + } + return null + } +} From 3533f14d216ebf6cf2d417e7d5c40ab30bf62e3b Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:08:58 +0700 Subject: [PATCH 04/10] refactor: adopt runScanner across all scanners --- src/scanners/apk.js | 54 ++++++++++------------------- src/scanners/apt.js | 42 ++++++++-------------- src/scanners/asdf.js | 53 +++++++++++----------------- src/scanners/brew.js | 56 +++++++++++------------------- src/scanners/bun.js | 41 +++++++--------------- src/scanners/choco.js | 54 +++++++++++------------------ src/scanners/composer.js | 42 +++++++--------------- src/scanners/conda.js | 42 +++++++--------------- src/scanners/flatpak.js | 51 +++++++++------------------ src/scanners/gem.js | 44 +++++++++-------------- src/scanners/helm.js | 55 +++++++++++------------------ src/scanners/krew.js | 46 ++++++++---------------- src/scanners/macports.js | 57 +++++++++++------------------- src/scanners/mise.js | 59 +++++++++++-------------------- src/scanners/nix.js | 40 +++++++-------------- src/scanners/npm.js | 70 ++++++++++++++++--------------------- src/scanners/opam.js | 60 +++++++++++--------------------- src/scanners/pacman.js | 55 +++++++++++------------------ src/scanners/pip.js | 35 +++++-------------- src/scanners/pipx.js | 45 ++++++++---------------- src/scanners/pkg.js | 57 +++++++++++------------------- src/scanners/pnpm.js | 43 ++++++++--------------- src/scanners/poetry.js | 51 +++++++++------------------ src/scanners/scoop.js | 45 +++++++++--------------- src/scanners/snap.js | 43 +++++++---------------- src/scanners/uv.js | 46 ++++++++---------------- src/scanners/vcpkg.js | 31 +++++------------ src/scanners/volta.js | 42 ++++++++-------------- src/scanners/winget.js | 75 ++++++++++++++++++---------------------- src/scanners/yarn.js | 41 +++++++--------------- src/scanners/yum.js | 54 +++++++++++------------------ src/scanners/zypper.js | 53 ++++++++++------------------ 32 files changed, 552 insertions(+), 1030 deletions(-) diff --git a/src/scanners/apk.js b/src/scanners/apk.js index 77a5d71..976e86b 100644 --- a/src/scanners/apk.js +++ b/src/scanners/apk.js @@ -1,41 +1,23 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('apk')) return null - try { - const raw = execSync('apk info -v', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const match = line.match(/^(.+)-([0-9][A-Za-z0-9._-]*)$/) - if (!match) { - return { name: line, version: 'unknown', type: 'system' } - } - - return { - name: match[1].trim(), - version: match[2].trim() || 'unknown', - type: 'system', - } - }) - .filter((pkg) => pkg.name) - - return { manager: 'apk', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ apk: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ apk: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'apk', + bin: 'apk', + command: 'apk info -v', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const match = line.match(/^(.+)-([0-9][A-Za-z0-9._-]*)$/) + return match + ? { name: match[1].trim(), version: match[2].trim() || 'unknown', type: 'system' } + : { name: line, version: 'unknown', type: 'system' } + }) + .filter((pkg) => pkg.name), + }) } diff --git a/src/scanners/apt.js b/src/scanners/apt.js index 226b2dc..ed42701 100644 --- a/src/scanners/apt.js +++ b/src/scanners/apt.js @@ -1,32 +1,20 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('dpkg-query')) return null - try { - const raw = execSync('dpkg-query -W -f="${Package}\t${Version}\n"', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = 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) - - return { manager: 'apt', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ apt: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ apt: scan timed out, skipping.') - } - return null - } + return runScanner({ + 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), + }) } diff --git a/src/scanners/asdf.js b/src/scanners/asdf.js index 575c765..84ab3d3 100644 --- a/src/scanners/asdf.js +++ b/src/scanners/asdf.js @@ -1,41 +1,28 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('asdf')) return null + return runScanner({ + manager: 'asdf', + bin: 'asdf', + command: 'asdf list', + permissionHint: 'permission denied.', + parse: (raw) => { + const packages = [] + let currentPlugin = null - try { - const raw = execSync('asdf list', { - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 10000, - }).toString() + for (const line of raw.split('\n')) { + if (!line.trim()) continue - const packages = [] - let currentPlugin = null - - for (const line of raw.split('\n')) { - if (!line.trim()) continue - - // Plugin names have no leading whitespace, versions are indented - if (!line.startsWith(' ') && !line.startsWith('\t')) { - currentPlugin = line.trim() - } else if (currentPlugin) { - const version = line.trim().replace(/^\*/, '').trim() - if (version) { - packages.push({ name: currentPlugin, version }) + // Plugin names have no leading whitespace, versions are indented + if (!line.startsWith(' ') && !line.startsWith('\t')) { + currentPlugin = line.trim() + } else if (currentPlugin) { + const version = line.trim().replace(/^\*/, '').trim() + if (version) packages.push({ name: currentPlugin, version }) } } - } - - if (packages.length === 0) return null - return { manager: 'asdf', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ asdf: permission denied.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ asdf: scan timed out, skipping.') - } - return null - } + return packages + }, + }) } diff --git a/src/scanners/brew.js b/src/scanners/brew.js index 16ef23d..0755449 100644 --- a/src/scanners/brew.js +++ b/src/scanners/brew.js @@ -1,40 +1,26 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('brew')) return null - try { - const raw = execSync('brew info --json=v2 --installed', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const parsed = JSON.parse(raw) - const formulae = parsed.formulae || [] - const casks = parsed.casks || [] - - const packages = [ - ...formulae.map((f) => ({ - name: f.name, - version: f.installed?.[0]?.version || 'unknown', - type: 'formula', - })), - ...casks.map((c) => ({ - name: c.token, - version: c.installed || 'unknown', - type: 'cask', - })), - ] - - return { manager: 'brew', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ brew: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ brew: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'brew', + bin: 'brew', + command: 'brew info --json=v2 --installed', + parse: (raw) => { + const parsed = JSON.parse(raw) + return [ + ...(parsed.formulae || []).map((f) => ({ + name: f.name, + version: f.installed?.[0]?.version || 'unknown', + type: 'formula', + })), + ...(parsed.casks || []).map((c) => ({ + name: c.token, + version: c.installed || 'unknown', + type: 'cask', + })), + ] + }, + }) } diff --git a/src/scanners/bun.js b/src/scanners/bun.js index 4d1a2b7..e81833c 100644 --- a/src/scanners/bun.js +++ b/src/scanners/bun.js @@ -1,31 +1,16 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('bun')) return null - - try { - const raw = execSync('bun pm ls --global --json', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const parsed = JSON.parse(raw) - const packages = (parsed.packages || parsed || []) - .map((pkg) => ({ - name: pkg.name, - version: pkg.version || 'unknown', - type: 'library', - })) - .filter((pkg) => pkg.name) - - return { manager: 'bun', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ bun: permission denied. Check bun global install permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ bun: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'bun', + bin: 'bun', + command: 'bun pm ls --global --json', + permissionHint: 'Check bun global install permissions.', + parse: (raw) => { + const parsed = JSON.parse(raw) + return (parsed.packages || parsed || []) + .map((pkg) => ({ name: pkg.name, version: pkg.version || 'unknown', type: 'library' })) + .filter((pkg) => pkg.name) + }, + }) } diff --git a/src/scanners/choco.js b/src/scanners/choco.js index 86c3ff1..fbca9b9 100644 --- a/src/scanners/choco.js +++ b/src/scanners/choco.js @@ -1,39 +1,25 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform !== 'win32') return null - if (!isAvailable('choco')) return null - try { - const raw = execSync('choco list --local-only --limit-output', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 30000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .filter((line) => !line.startsWith('Chocolatey v')) - .filter((line) => !line.toLowerCase().includes('packages installed')) - .map((line) => { - const [name, version] = line.split('|') - return { - name: name?.trim(), - version: version?.trim() || 'unknown', - type: 'system', - } - }) - .filter((pkg) => pkg.name) - - return { manager: 'choco', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ choco: permission denied. Try running in an elevated shell.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ choco: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'choco', + bin: 'choco', + command: 'choco list --local-only --limit-output', + timeout: 30000, + permissionHint: 'Try running in an elevated shell.', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !line.startsWith('Chocolatey v')) + .filter((line) => !line.toLowerCase().includes('packages installed')) + .map((line) => { + const [name, version] = line.split('|') + return { name: name?.trim(), version: version?.trim() || 'unknown', type: 'system' } + }) + .filter((pkg) => pkg.name), + }) } diff --git a/src/scanners/composer.js b/src/scanners/composer.js index f725105..e0ab508 100644 --- a/src/scanners/composer.js +++ b/src/scanners/composer.js @@ -1,32 +1,16 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('composer')) return null - - try { - const timeout = process.platform === 'win32' ? 30000 : 10000 - const raw = execSync('composer global show --format=json', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout, - }).toString() - - const parsed = JSON.parse(raw) - const installed = parsed.installed || [] - - const packages = installed.map((pkg) => ({ - name: pkg.name, - version: pkg.version || 'unknown', - type: 'php', - })) - - return { manager: 'composer', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ composer: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ composer: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'composer', + bin: 'composer', + command: 'composer global show --format=json', + timeout: process.platform === 'win32' ? 30000 : 10000, + parse: (raw) => + (JSON.parse(raw).installed || []).map((pkg) => ({ + name: pkg.name, + version: pkg.version || 'unknown', + type: 'php', + })), + }) } diff --git a/src/scanners/conda.js b/src/scanners/conda.js index b4464d5..58a33b0 100644 --- a/src/scanners/conda.js +++ b/src/scanners/conda.js @@ -1,32 +1,16 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - const cmd = isAvailable('mamba') ? 'mamba' : isAvailable('conda') ? 'conda' : null - if (!cmd) return null - - try { - const raw = execSync(`${cmd} list --json`, { - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 10000, - }).toString() - - const parsed = JSON.parse(raw) - if (!Array.isArray(parsed) || parsed.length === 0) return null - - const packages = parsed.map((pkg) => ({ - name: pkg.name, - version: pkg.version, - type: 'library', - })) - - return { manager: 'conda', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn(`⚠ ${cmd}: permission denied.`) - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn(`⚠ ${cmd}: scan timed out, skipping.`) - } - return null - } + return runScanner({ + manager: 'conda', + bin: ['mamba', 'conda'], + command: (bin) => `${bin} list --json`, + permissionHint: 'permission denied.', + parse: (raw) => { + const parsed = JSON.parse(raw) + return Array.isArray(parsed) + ? parsed.map((pkg) => ({ name: pkg.name, version: pkg.version, type: 'library' })) + : [] + }, + }) } diff --git a/src/scanners/flatpak.js b/src/scanners/flatpak.js index bf8af66..83645c0 100644 --- a/src/scanners/flatpak.js +++ b/src/scanners/flatpak.js @@ -1,37 +1,20 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('flatpak')) return null - - try { - const raw = execSync('flatpak list --app --columns=application,version', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .filter((line) => !line.startsWith('Application')) - .map((line) => { - const parts = line.split(/\s{2,}/) - return { - name: parts[0]?.trim(), - version: parts[1]?.trim() || 'unknown', - type: 'app', - } - }) - .filter((pkg) => pkg.name) - - return { manager: 'flatpak', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ flatpak: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ flatpak: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'flatpak', + bin: 'flatpak', + command: 'flatpak list --app --columns=application,version', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !line.startsWith('Application')) + .map((line) => { + const parts = line.split(/\s{2,}/) + return { name: parts[0]?.trim(), version: parts[1]?.trim() || 'unknown', type: 'app' } + }) + .filter((pkg) => pkg.name), + }) } diff --git a/src/scanners/gem.js b/src/scanners/gem.js index 544c25c..c6d72f3 100644 --- a/src/scanners/gem.js +++ b/src/scanners/gem.js @@ -1,34 +1,22 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('gem')) return null - try { - const raw = execSync('gem list', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = [] - const lines = raw.split('\n').filter(Boolean) - - for (const line of lines) { - const match = line.match(/^([^\s(]+)\s+\(([^)]+)\)/) - if (match) { - const versions = match[2].split(',').map((v) => v.trim()) - packages.push({ name: match[1].trim(), version: versions[0], type: 'gem' }) + return runScanner({ + manager: 'gem', + bin: 'gem', + command: 'gem list', + parse: (raw) => { + const packages = [] + for (const line of raw.split('\n').filter(Boolean)) { + const match = line.match(/^([^\s(]+)\s+\(([^)]+)\)/) + if (match) { + const versions = match[2].split(',').map((v) => v.trim()) + packages.push({ name: match[1].trim(), version: versions[0], type: 'gem' }) + } } - } - - return { manager: 'gem', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ gem: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ gem: scan timed out, skipping.') - } - return null - } + return packages + }, + }) } diff --git a/src/scanners/helm.js b/src/scanners/helm.js index 901c5b9..82134ba 100644 --- a/src/scanners/helm.js +++ b/src/scanners/helm.js @@ -1,38 +1,23 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('helm')) return null - - try { - const raw = execSync('helm plugin list', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .filter((line) => !line.startsWith('NAME')) - .map((line) => { - const parts = line.split(/\s{2,}|\t+/).filter(Boolean) - if (parts.length < 2) return null - return { - name: parts[0], - version: parts[1] || 'unknown', - type: 'plugin', - } - }) - .filter((pkg) => pkg?.name) - - return { manager: 'helm', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ helm: permission denied. Check Helm plugin permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ helm: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'helm', + bin: 'helm', + command: 'helm plugin list', + permissionHint: 'Check Helm plugin permissions.', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !line.startsWith('NAME')) + .map((line) => { + const parts = line.split(/\s{2,}|\t+/).filter(Boolean) + return parts.length < 2 + ? null + : { name: parts[0], version: parts[1] || 'unknown', type: 'plugin' } + }) + .filter((pkg) => pkg?.name), + }) } diff --git a/src/scanners/krew.js b/src/scanners/krew.js index c8888b4..0458ec8 100644 --- a/src/scanners/krew.js +++ b/src/scanners/krew.js @@ -1,34 +1,18 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('kubectl')) return null - - try { - const raw = execSync('kubectl krew list', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .filter((line) => line !== 'PLUGIN') - .map((line) => ({ - name: line, - version: 'installed', - type: 'plugin', - })) - .filter((pkg) => pkg.name) - - return { manager: 'krew', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ krew: permission denied. Check Krew permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ krew: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'krew', + bin: 'kubectl', + command: 'kubectl krew list', + permissionHint: 'Check Krew permissions.', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => line !== 'PLUGIN') + .map((line) => ({ name: line, version: 'installed', type: 'plugin' })) + .filter((pkg) => pkg.name), + }) } diff --git a/src/scanners/macports.js b/src/scanners/macports.js index 6fa1ac4..cba5e18 100644 --- a/src/scanners/macports.js +++ b/src/scanners/macports.js @@ -1,42 +1,25 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('port')) return null - try { - const raw = execSync('port installed', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .filter((line) => !line.startsWith('The following ports')) - .map((line) => { - const match = line.match(/^([^\s@]+)\s+@([^\s]+)(?:\s+\(([^)]+)\))?$/) - if (!match) return null - - return { - name: match[1].trim(), - version: match[2].trim() || 'unknown', - type: 'port', - } - }) - .filter((pkg) => pkg?.name) - - if (packages.length === 0) return null - - return { manager: 'macports', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ macports: permission denied. Check MacPorts permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ macports: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'macports', + bin: 'port', + command: 'port installed', + permissionHint: 'Check MacPorts permissions.', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !line.startsWith('The following ports')) + .map((line) => { + const match = line.match(/^([^\s@]+)\s+@([^\s]+)(?:\s+\(([^)]+)\))?$/) + return match + ? { name: match[1].trim(), version: match[2].trim() || 'unknown', type: 'port' } + : null + }) + .filter((pkg) => pkg?.name), + }) } diff --git a/src/scanners/mise.js b/src/scanners/mise.js index ef73bc0..cbc6beb 100644 --- a/src/scanners/mise.js +++ b/src/scanners/mise.js @@ -1,46 +1,29 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('mise')) return null + return runScanner({ + manager: 'mise', + bin: 'mise', + command: 'mise ls --json', + permissionHint: 'permission denied.', + parse: (raw) => { + const parsed = JSON.parse(raw) + const packages = [] - try { - const raw = execSync('mise ls --json', { - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 10000, - }).toString() - - const parsed = JSON.parse(raw) - - const packages = [] - - if (Array.isArray(parsed)) { - for (const entry of parsed) { - if (entry.tool && entry.version) { - packages.push({ name: entry.tool, version: entry.version }) + if (Array.isArray(parsed)) { + for (const entry of parsed) { + if (entry.tool && entry.version) + packages.push({ name: entry.tool, version: entry.version }) } - } - } else if (typeof parsed === 'object') { - for (const [name, versions] of Object.entries(parsed)) { - const list = Array.isArray(versions) ? versions : [versions] - for (const v of list) { - packages.push({ - name, - version: typeof v === 'string' ? v : v.version || 'installed', - }) + } else if (typeof parsed === 'object') { + for (const [name, versions] of Object.entries(parsed)) { + for (const v of Array.isArray(versions) ? versions : [versions]) { + packages.push({ name, version: typeof v === 'string' ? v : v.version || 'installed' }) + } } } - } - - if (packages.length === 0) return null - return { manager: 'mise', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ mise: permission denied.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ mise: scan timed out, skipping.') - } - return null - } + return packages + }, + }) } diff --git a/src/scanners/nix.js b/src/scanners/nix.js index 32ff3be..7e8f624 100644 --- a/src/scanners/nix.js +++ b/src/scanners/nix.js @@ -1,31 +1,15 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('nix-env')) return null - - try { - const raw = execSync('nix-env -q --installed --json', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: process.platform === 'win32' ? 30000 : 15000, - }).toString() - - const parsed = JSON.parse(raw) - const packages = Object.entries(parsed) - .map(([name, info]) => ({ - name, - version: info?.version || 'unknown', - type: 'system', - })) - .filter((pkg) => pkg.name) - - return { manager: 'nix', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ nix: permission denied. Check nix profile permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ nix: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'nix', + bin: 'nix-env', + command: 'nix-env -q --installed --json', + timeout: process.platform === 'win32' ? 30000 : 15000, + permissionHint: 'Check nix profile permissions.', + parse: (raw) => + Object.entries(JSON.parse(raw)) + .map(([name, info]) => ({ name, version: info?.version || 'unknown', type: 'system' })) + .filter((pkg) => pkg.name), + }) } diff --git a/src/scanners/npm.js b/src/scanners/npm.js index 43311bb..8b3d63c 100644 --- a/src/scanners/npm.js +++ b/src/scanners/npm.js @@ -1,50 +1,38 @@ import { execSync } from 'child_process' import { readFileSync } from 'fs' import { join } from 'path' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('npm')) return null + return runScanner({ + manager: 'npm', + bin: 'npm', + command: 'npm list -g --depth=0 --json', + timeout: process.platform === 'win32' ? 30000 : 10000, + parse: (raw) => { + const deps = JSON.parse(raw).dependencies || {} - try { - const timeout = process.platform === 'win32' ? 30000 : 10000 - const raw = execSync('npm list -g --depth=0 --json', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout, - }).toString() - - const parsed = JSON.parse(raw) - const deps = parsed.dependencies || {} - - let globalRoot = '' - try { - globalRoot = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] }) - .toString() - .trim() - } catch (_err) { - /* npm root -g unavailable */ - } - - const packages = Object.entries(deps).map(([name, info]) => { - let type = 'library' - if (globalRoot) { - try { - const pkgJson = JSON.parse(readFileSync(join(globalRoot, name, 'package.json'), 'utf8')) - if (pkgJson.bin) type = 'cli' - } catch (_err) { - /* package.json unreadable, default to library */ - } + let globalRoot = '' + try { + globalRoot = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] }) + .toString() + .trim() + } catch { + /* npm root -g unavailable */ } - return { name, version: info.version || 'unknown', type } - }) - return { manager: 'npm', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ npm: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ npm: scan timed out, skipping.') - } - return null - } + return Object.entries(deps).map(([name, info]) => { + let type = 'library' + if (globalRoot) { + try { + const pkgJson = JSON.parse(readFileSync(join(globalRoot, name, 'package.json'), 'utf8')) + if (pkgJson.bin) type = 'cli' + } catch { + /* package.json unreadable, default to library */ + } + } + return { name, version: info.version || 'unknown', type } + }) + }, + }) } diff --git a/src/scanners/opam.js b/src/scanners/opam.js index c653249..073ad4e 100644 --- a/src/scanners/opam.js +++ b/src/scanners/opam.js @@ -1,42 +1,24 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('opam')) return null - - try { - const raw = execSync('opam list --installed --columns=name,version --color=never', { - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .filter((line) => !line.startsWith('#')) - .filter((line) => !/^name\s+version$/i.test(line)) - .map((line) => { - const parts = line.split(/\s+/) - if (parts.length < 2) return null - - return { - name: parts[0], - version: parts[1] || 'unknown', - type: 'library', - } - }) - .filter((pkg) => pkg?.name) - - if (packages.length === 0) return null - - return { manager: 'opam', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ opam: permission denied. Check OPAM permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ opam: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'opam', + bin: 'opam', + command: 'opam list --installed --columns=name,version --color=never', + permissionHint: 'Check OPAM permissions.', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !line.startsWith('#')) + .filter((line) => !/^name\s+version$/i.test(line)) + .map((line) => { + const parts = line.split(/\s+/) + return parts.length < 2 + ? null + : { name: parts[0], version: parts[1] || 'unknown', type: 'library' } + }) + .filter((pkg) => pkg?.name), + }) } diff --git a/src/scanners/pacman.js b/src/scanners/pacman.js index c6baa19..a5e153b 100644 --- a/src/scanners/pacman.js +++ b/src/scanners/pacman.js @@ -1,39 +1,26 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('pacman')) return null - try { - const raw = execSync('pacman -Q', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const firstSpace = line.indexOf(' ') - if (firstSpace === -1) return null - - return { - name: line.slice(0, firstSpace).trim(), - version: line.slice(firstSpace + 1).trim() || 'unknown', - type: 'system', - } - }) - .filter((pkg) => pkg?.name) - - return { manager: 'pacman', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ pacman: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ pacman: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'pacman', + bin: 'pacman', + command: 'pacman -Q', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const firstSpace = line.indexOf(' ') + if (firstSpace === -1) return null + return { + name: line.slice(0, firstSpace).trim(), + version: line.slice(firstSpace + 1).trim() || 'unknown', + type: 'system', + } + }) + .filter((pkg) => pkg?.name), + }) } diff --git a/src/scanners/pip.js b/src/scanners/pip.js index 2abcfdb..3af631f 100644 --- a/src/scanners/pip.js +++ b/src/scanners/pip.js @@ -1,30 +1,11 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - const cmd = isAvailable('pip3') ? 'pip3' : isAvailable('pip') ? 'pip' : null - if (!cmd) return null - - try { - const raw = execSync(`${cmd} list --format=json`, { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const parsed = JSON.parse(raw) - const packages = parsed.map((pkg) => ({ - name: pkg.name, - version: pkg.version, - type: 'library', - })) - - return { manager: 'pip', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ pip: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ pip: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'pip', + bin: ['pip3', 'pip'], + command: (bin) => `${bin} list --format=json`, + parse: (raw) => + JSON.parse(raw).map((pkg) => ({ name: pkg.name, version: pkg.version, type: 'library' })), + }) } diff --git a/src/scanners/pipx.js b/src/scanners/pipx.js index 9ab006b..cf64edc 100644 --- a/src/scanners/pipx.js +++ b/src/scanners/pipx.js @@ -1,33 +1,18 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('pipx')) return null - - try { - const raw = execSync('pipx list --json', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const parsed = JSON.parse(raw) - const venvs = parsed.venvs || {} - - const packages = Object.entries(venvs) - .map(([name, info]) => ({ - name, - version: info?.metadata?.main_package?.package_version || 'unknown', - type: 'cli', - })) - .filter((pkg) => pkg.name) - - return { manager: 'pipx', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ pipx: permission denied. Check pipx permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ pipx: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'pipx', + bin: 'pipx', + command: 'pipx list --json', + permissionHint: 'Check pipx permissions.', + parse: (raw) => + Object.entries(JSON.parse(raw).venvs || {}) + .map(([name, info]) => ({ + name, + version: info?.metadata?.main_package?.package_version || 'unknown', + type: 'cli', + })) + .filter((pkg) => pkg.name), + }) } diff --git a/src/scanners/pkg.js b/src/scanners/pkg.js index 3ca4fa8..9a6ec7d 100644 --- a/src/scanners/pkg.js +++ b/src/scanners/pkg.js @@ -1,41 +1,26 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (!['freebsd', 'openbsd'].includes(process.platform)) return null - if (!isAvailable('pkg')) return null - try { - const raw = execSync('pkg info', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const lastDash = line.lastIndexOf('-') - if (lastDash === -1) { - return { name: line, version: 'unknown', type: 'system' } - } - - return { - name: line.slice(0, lastDash).trim(), - version: line.slice(lastDash + 1).trim() || 'unknown', - type: 'system', - } - }) - .filter((pkg) => pkg.name) - - return { manager: 'pkg', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ pkg: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ pkg: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'pkg', + bin: 'pkg', + command: 'pkg info', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const lastDash = line.lastIndexOf('-') + if (lastDash === -1) return { name: line, version: 'unknown', type: 'system' } + return { + name: line.slice(0, lastDash).trim(), + version: line.slice(lastDash + 1).trim() || 'unknown', + type: 'system', + } + }) + .filter((pkg) => pkg.name), + }) } diff --git a/src/scanners/pnpm.js b/src/scanners/pnpm.js index d73a923..5ea8223 100644 --- a/src/scanners/pnpm.js +++ b/src/scanners/pnpm.js @@ -1,31 +1,18 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('pnpm')) return null - - try { - const raw = execSync('pnpm list -g --depth=0 --json', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const parsed = JSON.parse(raw) - const deps = Array.isArray(parsed) ? parsed[0]?.dependencies || {} : parsed.dependencies || {} - - const packages = Object.entries(deps).map(([name, info]) => ({ - name, - version: info.version || 'unknown', - type: 'cli', - })) - - return { manager: 'pnpm', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ pnpm: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ pnpm: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'pnpm', + bin: 'pnpm', + command: 'pnpm list -g --depth=0 --json', + parse: (raw) => { + const parsed = JSON.parse(raw) + const deps = Array.isArray(parsed) ? parsed[0]?.dependencies || {} : parsed.dependencies || {} + return Object.entries(deps).map(([name, info]) => ({ + name, + version: info.version || 'unknown', + type: 'cli', + })) + }, + }) } diff --git a/src/scanners/poetry.js b/src/scanners/poetry.js index c39dccb..e72c8ca 100644 --- a/src/scanners/poetry.js +++ b/src/scanners/poetry.js @@ -1,37 +1,20 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('poetry')) return null - - try { - const raw = execSync('poetry self show plugins --no-ansi', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const match = line.match(/^([^\s]+)\s+([0-9][^\s]*)/) - if (!match) return null - return { - name: match[1].trim(), - version: match[2].trim(), - type: 'plugin', - } - }) - .filter((pkg) => pkg?.name) - - return { manager: 'poetry', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ poetry: permission denied. Check Poetry permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ poetry: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'poetry', + bin: 'poetry', + command: 'poetry self show plugins --no-ansi', + permissionHint: 'Check Poetry permissions.', + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const match = line.match(/^([^\s]+)\s+([0-9][^\s]*)/) + return match ? { name: match[1].trim(), version: match[2].trim(), type: 'plugin' } : null + }) + .filter((pkg) => pkg?.name), + }) } diff --git a/src/scanners/scoop.js b/src/scanners/scoop.js index 42b13e4..7062aef 100644 --- a/src/scanners/scoop.js +++ b/src/scanners/scoop.js @@ -1,34 +1,21 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform !== 'win32') return null - if (!isAvailable('scoop')) return null - try { - const raw = execSync('scoop export', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 30000, - }).toString() - - const parsed = JSON.parse(raw) - const apps = parsed.apps || [] - - const packages = apps - .map((app) => ({ - name: app.Name || app.name, - version: app.Version || app.version || 'unknown', - type: 'system', - })) - .filter((pkg) => pkg.name) - - return { manager: 'scoop', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ scoop: permission denied. Try running in an elevated shell.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ scoop: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'scoop', + bin: 'scoop', + command: 'scoop export', + timeout: 30000, + permissionHint: 'Try running in an elevated shell.', + parse: (raw) => + (JSON.parse(raw).apps || []) + .map((app) => ({ + name: app.Name || app.name, + version: app.Version || app.version || 'unknown', + type: 'system', + })) + .filter((pkg) => pkg.name), + }) } diff --git a/src/scanners/snap.js b/src/scanners/snap.js index 9fa8aa4..2d1b249 100644 --- a/src/scanners/snap.js +++ b/src/scanners/snap.js @@ -1,34 +1,17 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('snap')) return null - - try { - const raw = execSync('snap list', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const lines = raw.split('\n').filter(Boolean) - const packages = [] - - for (const line of lines.slice(1)) { - const trimmed = line.trim() - if (!trimmed) continue - const parts = trimmed.split(/\s+/) - if (parts.length >= 2) { - packages.push({ name: parts[0], version: parts[1], type: 'snap' }) + return runScanner({ + manager: 'snap', + bin: 'snap', + command: 'snap list', + parse: (raw) => { + const packages = [] + for (const line of raw.split('\n').filter(Boolean).slice(1)) { + const parts = line.trim().split(/\s+/) + if (parts.length >= 2) packages.push({ name: parts[0], version: parts[1], type: 'snap' }) } - } - - return { manager: 'snap', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ snap: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ snap: scan timed out, skipping.') - } - return null - } + return packages + }, + }) } diff --git a/src/scanners/uv.js b/src/scanners/uv.js index 0894b64..4935460 100644 --- a/src/scanners/uv.js +++ b/src/scanners/uv.js @@ -1,36 +1,18 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('uv')) return null - - try { - const raw = execSync('uv tool list', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = [] - const lines = raw.split('\n').filter(Boolean) - - for (const line of lines) { - const match = line.trim().match(/^([^\s]+)\s+v([^\s]+)/) - if (match) { - packages.push({ - name: match[1].trim(), - version: match[2].trim(), - type: 'cli', - }) + return runScanner({ + manager: 'uv', + bin: 'uv', + command: 'uv tool list', + permissionHint: 'Check uv tool permissions.', + parse: (raw) => { + const packages = [] + for (const line of raw.split('\n').filter(Boolean)) { + const match = line.trim().match(/^([^\s]+)\s+v([^\s]+)/) + if (match) packages.push({ name: match[1].trim(), version: match[2].trim(), type: 'cli' }) } - } - - return { manager: 'uv', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ uv: permission denied. Check uv tool permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ uv: scan timed out, skipping.') - } - return null - } + return packages + }, + }) } diff --git a/src/scanners/vcpkg.js b/src/scanners/vcpkg.js index 3b29e92..3f25875 100644 --- a/src/scanners/vcpkg.js +++ b/src/scanners/vcpkg.js @@ -1,5 +1,4 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export function parseVcpkgList(raw) { return raw @@ -21,25 +20,11 @@ export function parseVcpkgList(raw) { } export default async function scan() { - if (!isAvailable('vcpkg')) return null - - try { - const raw = execSync('vcpkg list', { - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 10000, - }).toString() - - const packages = parseVcpkgList(raw) - - if (packages.length === 0) return null - - return { manager: 'vcpkg', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ vcpkg: permission denied. Check vcpkg permissions.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ vcpkg: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'vcpkg', + bin: 'vcpkg', + command: 'vcpkg list', + permissionHint: 'Check vcpkg permissions.', + parse: parseVcpkgList, + }) } diff --git a/src/scanners/volta.js b/src/scanners/volta.js index 6b34ede..5b16e69 100644 --- a/src/scanners/volta.js +++ b/src/scanners/volta.js @@ -1,32 +1,18 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('volta')) return null - - try { - const raw = execSync('volta list --format=plain', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = [] - const lines = raw.split('\n').filter(Boolean) - - for (const line of lines) { - const match = line.match(/^(?:tool\s+)?([^\s@]+)@(\S+)/) - if (match) { - packages.push({ name: match[1].trim(), version: match[2].trim(), type: 'runtime' }) + return runScanner({ + manager: 'volta', + bin: 'volta', + command: 'volta list --format=plain', + parse: (raw) => { + const packages = [] + for (const line of raw.split('\n').filter(Boolean)) { + const match = line.match(/^(?:tool\s+)?([^\s@]+)@(\S+)/) + if (match) + packages.push({ name: match[1].trim(), version: match[2].trim(), type: 'runtime' }) } - } - - return { manager: 'volta', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ volta: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ volta: scan timed out, skipping.') - } - return null - } + return packages + }, + }) } diff --git a/src/scanners/winget.js b/src/scanners/winget.js index 5302628..96c4aff 100644 --- a/src/scanners/winget.js +++ b/src/scanners/winget.js @@ -1,46 +1,39 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform !== 'win32') return null - if (!isAvailable('winget')) return null - try { - const raw = execSync('winget list --accept-source-agreements', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 30000, - }).toString() - - const lines = raw - .split('\n') - .map((line) => line.replace(/\r$/, '')) - .filter(Boolean) - - const separatorIndex = lines.findIndex((line) => /^[-\s]+$/.test(line)) - if (separatorIndex === -1) return null - - const packages = [] - for (const line of lines.slice(separatorIndex + 1)) { - const trimmed = line.trim() - if (!trimmed) continue - - const match = trimmed.match(/^(.+?)\s{2,}(\S+)?\s{2,}(\S+)?\s{2,}(\S+)?(?:\s{2,}.*)?$/) - if (!match) continue - - packages.push({ - name: match[1]?.trim(), - version: match[3]?.trim() || 'unknown', - type: 'system', - }) - } - - return { manager: 'winget', packages: packages.filter((pkg) => pkg.name) } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ winget: permission denied. Try running in an elevated shell.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ winget: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'winget', + bin: 'winget', + command: 'winget list --accept-source-agreements', + timeout: 30000, + permissionHint: 'Try running in an elevated shell.', + parse: (raw) => { + const lines = raw + .split('\n') + .map((line) => line.replace(/\r$/, '')) + .filter(Boolean) + + const separatorIndex = lines.findIndex((line) => /^[-\s]+$/.test(line)) + if (separatorIndex === -1) return [] + + const packages = [] + for (const line of lines.slice(separatorIndex + 1)) { + const trimmed = line.trim() + if (!trimmed) continue + + const match = trimmed.match(/^(.+?)\s{2,}(\S+)?\s{2,}(\S+)?\s{2,}(\S+)?(?:\s{2,}.*)?$/) + if (!match) continue + + packages.push({ + name: match[1]?.trim(), + version: match[3]?.trim() || 'unknown', + type: 'system', + }) + } + + return packages.filter((pkg) => pkg.name) + }, + }) } diff --git a/src/scanners/yarn.js b/src/scanners/yarn.js index 0816898..9e9f11d 100644 --- a/src/scanners/yarn.js +++ b/src/scanners/yarn.js @@ -1,32 +1,17 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { - if (!isAvailable('yarn')) return null - - try { - const raw = execSync('yarn global list --depth=0 2>/dev/null', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = [] - const lines = raw.split('\n') - - for (const line of lines) { - const match = line.match(/info\s+"([^@]+)@([^"]+)"/) || line.match(/[└├─]+\s+([^@]+)@(\S+)/) - if (match) { - packages.push({ name: match[1].trim(), version: match[2].trim(), type: 'cli' }) + return runScanner({ + manager: 'yarn', + bin: 'yarn', + command: 'yarn global list --depth=0 2>/dev/null', + parse: (raw) => { + const packages = [] + for (const line of raw.split('\n')) { + const match = line.match(/info\s+"([^@]+)@([^"]+)"/) || line.match(/[└├─]+\s+([^@]+)@(\S+)/) + if (match) packages.push({ name: match[1].trim(), version: match[2].trim(), type: 'cli' }) } - } - - return { manager: 'yarn', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ yarn: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ yarn: scan timed out, skipping.') - } - return null - } + return packages + }, + }) } diff --git a/src/scanners/yum.js b/src/scanners/yum.js index 5cd9f52..038dcb2 100644 --- a/src/scanners/yum.js +++ b/src/scanners/yum.js @@ -1,40 +1,26 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('yum')) return null - try { - const raw = execSync('yum list installed -q', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 10000, - }).toString() - - const packages = [] - const lines = raw.split('\n').filter(Boolean) - - for (const line of lines) { - if (line.startsWith('Installed Packages')) continue - - const match = line.match(/^([^\s]+)\s+([^\s]+)\s+/) - if (match) { - const pkgName = match[1].replace(/\.[^.\s]+$/, '') - packages.push({ - name: pkgName, - version: match[2].trim(), - type: 'system', - }) + return runScanner({ + manager: 'yum', + bin: 'yum', + command: 'yum list installed -q', + parse: (raw) => { + const packages = [] + for (const line of raw.split('\n').filter(Boolean)) { + if (line.startsWith('Installed Packages')) continue + const match = line.match(/^([^\s]+)\s+([^\s]+)\s+/) + if (match) { + packages.push({ + name: match[1].replace(/\.[^.\s]+$/, ''), + version: match[2].trim(), + type: 'system', + }) + } } - } - - return { manager: 'yum', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ yum: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ yum: scan timed out, skipping.') - } - return null - } + return packages + }, + }) } diff --git a/src/scanners/zypper.js b/src/scanners/zypper.js index 3d58e36..73b7e35 100644 --- a/src/scanners/zypper.js +++ b/src/scanners/zypper.js @@ -1,39 +1,24 @@ -import { execSync } from 'child_process' -import { isAvailable } from '../utils.js' +import { runScanner } from '../utils.js' export default async function scan() { if (process.platform === 'win32') return null - if (!isAvailable('zypper')) return null - try { - const raw = execSync('zypper search --installed-only --details --type package', { - stdio: ['ignore', 'pipe', 'ignore'], - timeout: 15000, - }).toString() - - const packages = raw - .split('\n') - .map((line) => line.trim()) - .filter((line) => line.startsWith('i |')) - .map((line) => { - const parts = line.split('|').map((part) => part.trim()) - if (parts.length < 5) return null - - return { - name: parts[2], - version: parts[4] || 'unknown', - type: 'system', - } - }) - .filter((pkg) => pkg?.name) - - return { manager: 'zypper', packages } - } catch (err) { - if (err.message?.includes('EACCES') || err.message?.includes('permission')) { - console.warn('⚠ zypper: permission denied. Try running with sudo.') - } else if (err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT') { - console.warn('⚠ zypper: scan timed out, skipping.') - } - return null - } + return runScanner({ + manager: 'zypper', + bin: 'zypper', + command: 'zypper search --installed-only --details --type package', + timeout: 15000, + parse: (raw) => + raw + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.startsWith('i |')) + .map((line) => { + const parts = line.split('|').map((part) => part.trim()) + return parts.length < 5 + ? null + : { name: parts[2], version: parts[4] || 'unknown', type: 'system' } + }) + .filter((pkg) => pkg?.name), + }) } From b5790762c55281d500b9e02fec550c23b4530415 Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:10:41 +0700 Subject: [PATCH 05/10] refactor: extract scanAll scanner-resolution helper --- src/audit.js | 28 ++++------------------ src/index.js | 64 ++++++++++++++++++++++++++++++-------------------- src/upgrade.js | 23 ++++-------------- 3 files changed, 48 insertions(+), 67 deletions(-) diff --git a/src/audit.js b/src/audit.js index 553f069..14be821 100644 --- a/src/audit.js +++ b/src/audit.js @@ -3,7 +3,7 @@ import chalk from 'chalk' import Table from 'cli-table3' import { renderBanner, MANAGER_ICONS } from './display/table.js' -import { ALL_SCANNERS, printIssueSummary } from './index.js' +import { scanAll, printIssueSummary } from './index.js' const OSV_QUERY_BATCH_URL = 'https://api.osv.dev/v1/querybatch' const BATCH_SIZE = 100 @@ -231,34 +231,14 @@ export async function runAudit(options) { resolvedOptions?.json || parentOptions.json || hasCliFlag(['--json', '-j']) ) - let scanners = Object.entries(ALL_SCANNERS) - if (filterManager) { - const selected = ALL_SCANNERS[filterManager] - if (!selected) { - console.error(chalk.red(`✗ Unknown manager: "${filterManager}"`)) - console.error(` Available: ${Object.keys(ALL_SCANNERS).join(', ')}`) - process.exit(1) - } - scanners = [[filterManager, selected]] - } - const spinner = doJson ? { stop() {} } : ora('Checking package audit status...').start() const scanIssues = [] try { - const settled = await Promise.allSettled(scanners.map(([_name, scanFn]) => scanFn())) + const { results: scannedRaw, issues } = await scanAll(filterManager) + scanIssues.push(...issues) - const scanned = settled - .map((entry, index) => { - if (entry.status === 'fulfilled') return entry.value - scanIssues.push({ - manager: scanners[index][0], - message: entry.reason?.message || 'scan failed unexpectedly', - level: 'error', - }) - return null - }) - .filter(Boolean) + const scanned = scannedRaw .map((result) => ({ ...result, packages: packageFilter diff --git a/src/index.js b/src/index.js index fd787b8..9addc41 100644 --- a/src/index.js +++ b/src/index.js @@ -127,6 +127,42 @@ export const ALL_SCANNERS = { vcpkg: vcpkgScanner, } +// Resolve the scanner list for an optional manager filter; exit on unknown name. +export function resolveScanners(filterManager) { + if (!filterManager) return Object.entries(ALL_SCANNERS) + + const key = filterManager.toLowerCase() + const selected = ALL_SCANNERS[key] + if (!selected) { + console.error(chalk.red(`✗ Unknown manager: "${filterManager}"`)) + console.error(` Available: ${Object.keys(ALL_SCANNERS).join(', ')}`) + process.exit(1) + } + return [[key, selected]] +} + +// Run every selected scanner in parallel; split into results and error issues. +export async function scanAll(filterManager) { + const scanners = resolveScanners(filterManager) + const settled = await Promise.allSettled(scanners.map(([, scanFn]) => scanFn())) + + const results = [] + const issues = [] + settled.forEach((entry, index) => { + if (entry.status === 'fulfilled') { + if (entry.value) results.push(entry.value) + } else { + issues.push({ + manager: scanners[index][0], + message: entry.reason?.message || 'scan failed unexpectedly', + level: 'error', + }) + } + }) + + return { results, issues } +} + export async function run(options) { const resolvedOptions = typeof options?.opts === 'function' ? options.opts() : options const { @@ -137,18 +173,6 @@ export async function run(options) { json: doJson, } = resolvedOptions - let scanners = Object.entries(ALL_SCANNERS) - - if (filterManager) { - const selected = ALL_SCANNERS[filterManager.toLowerCase()] - if (!selected) { - console.error(chalk.red(`✗ Unknown manager: "${filterManager}"`)) - console.error(` Available: ${Object.keys(ALL_SCANNERS).join(', ')}`) - process.exit(1) - } - scanners = [[filterManager.toLowerCase(), selected]] - } - const spinner = ora('Scanning package managers...').start() const scanIssues = [] @@ -164,24 +188,14 @@ export async function run(options) { }) } - const settled = await Promise.allSettled(scanners.map(([_name, scanFn]) => scanFn())) + const { results: scanned, issues } = await scanAll(filterManager) console.warn = originalWarn spinner.stop() - settled.forEach((entry, index) => { - if (entry.status === 'rejected') { - scanIssues.push({ - manager: scanners[index][0], - message: entry.reason?.message || 'scan failed unexpectedly', - level: 'error', - }) - } - }) + scanIssues.push(...issues) - let results = settled - .map((s) => (s.status === 'fulfilled' ? s.value : null)) - .filter((r) => r && r.packages.length > 0) + let results = scanned.filter((r) => r && r.packages.length > 0) if (results.length === 0) { console.log(chalk.yellow('No package managers found or all scans failed.')) diff --git a/src/upgrade.js b/src/upgrade.js index 797e570..1c4f932 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -2,7 +2,7 @@ import chalk from 'chalk' import ora from 'ora' import { spawnSync } from 'child_process' -import { ALL_SCANNERS, printIssueSummary } from './index.js' +import { scanAll, printIssueSummary } from './index.js' import { isAvailable } from './utils.js' import { renderBanner, MANAGER_ICONS } from './display/table.js' @@ -161,26 +161,13 @@ function renderUpgradeResults(results) { } async function detectInstalledManagers(filterManager) { - let scanners = Object.entries(ALL_SCANNERS) - - if (filterManager) { - const selected = ALL_SCANNERS[filterManager] - if (!selected) { - console.error(chalk.red(`✗ Unknown manager: "${filterManager}"`)) - console.error(` Available: ${Object.keys(ALL_SCANNERS).join(', ')}`) - process.exit(1) - } - scanners = [[filterManager, selected]] - } - const spinner = ora('Detecting installed package managers...').start() - const settled = await Promise.allSettled(scanners.map(([_name, scanFn]) => scanFn())) + const { results } = await scanAll(filterManager) spinner.stop() - return settled - .map((entry, index) => ({ entry, manager: scanners[index][0] })) - .filter(({ entry }) => entry.status === 'fulfilled' && entry.value?.packages?.length > 0) - .map(({ entry, manager }) => ({ manager, packages: entry.value.packages })) + return results + .filter((result) => result.packages?.length > 0) + .map((result) => ({ manager: result.manager, packages: result.packages })) } export async function runUpgrade(options) { From b07a3a63b0430ca896f49d2eb79e87fe997acfb8 Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:11:54 +0700 Subject: [PATCH 06/10] refactor: dedupe CLI-arg helpers into utils --- src/audit.js | 17 ++--------------- src/index.js | 3 ++- src/ports.js | 4 ++-- src/upgrade.js | 18 ++---------------- src/utils.js | 17 +++++++++++++++++ 5 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/audit.js b/src/audit.js index 14be821..4b6e2ca 100644 --- a/src/audit.js +++ b/src/audit.js @@ -4,6 +4,7 @@ import Table from 'cli-table3' import { renderBanner, MANAGER_ICONS } from './display/table.js' import { scanAll, printIssueSummary } from './index.js' +import { getCliOptionValue, hasCliFlag, optsOf } from './utils.js' const OSV_QUERY_BATCH_URL = 'https://api.osv.dev/v1/querybatch' const BATCH_SIZE = 100 @@ -148,20 +149,6 @@ function renderAuditResults(results) { console.log() } -function getCliOptionValue(flags) { - for (let index = 0; index < process.argv.length; index += 1) { - const token = process.argv[index] - if (!flags.includes(token)) continue - return process.argv[index + 1] - } - - return undefined -} - -function hasCliFlag(flags) { - return process.argv.some((token) => flags.includes(token)) -} - function chunk(items, size) { const batches = [] for (let index = 0; index < items.length; index += size) { @@ -217,7 +204,7 @@ async function auditPackages(manager, packages) { } export async function runAudit(options) { - const resolvedOptions = typeof options?.opts === 'function' ? options.opts() : options + const resolvedOptions = optsOf(options) const parentOptions = options?.parent?.opts?.() || {} const filterManager = ( resolvedOptions?.manager || diff --git a/src/index.js b/src/index.js index 9addc41..84c3bcc 100644 --- a/src/index.js +++ b/src/index.js @@ -41,6 +41,7 @@ import macportsScanner from './scanners/macports.js' import opamScanner from './scanners/opam.js' import vcpkgScanner from './scanners/vcpkg.js' import { renderAll } from './display/table.js' +import { optsOf } from './utils.js' export function normalizeWarning(args) { return args @@ -164,7 +165,7 @@ export async function scanAll(filterManager) { } export async function run(options) { - const resolvedOptions = typeof options?.opts === 'function' ? options.opts() : options + const resolvedOptions = optsOf(options) const { manager: filterManager, search: searchTerm, diff --git a/src/ports.js b/src/ports.js index 1f339ec..7e874c5 100644 --- a/src/ports.js +++ b/src/ports.js @@ -4,7 +4,7 @@ import { execSync } from 'child_process' import { renderBanner } from './display/table.js' import { renderPorts } from './display/ports.js' -import { isAvailable } from './utils.js' +import { isAvailable, optsOf } from './utils.js' function parsePsRow(raw) { const trimmed = raw.trim() @@ -279,7 +279,7 @@ function getActivePorts() { } export async function runPorts(options) { - const resolvedOptions = typeof options?.opts === 'function' ? options.opts() : options + const resolvedOptions = optsOf(options) const doJson = Boolean( resolvedOptions?.json || options?.parent?.opts?.().json || diff --git a/src/upgrade.js b/src/upgrade.js index 1c4f932..3addcbc 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -3,7 +3,7 @@ import ora from 'ora' import { spawnSync } from 'child_process' import { scanAll, printIssueSummary } from './index.js' -import { isAvailable } from './utils.js' +import { isAvailable, getCliOptionValue, hasCliFlag, optsOf } from './utils.js' import { renderBanner, MANAGER_ICONS } from './display/table.js' export const UPGRADE_PLANS = { @@ -69,20 +69,6 @@ export const UPGRADE_PLANS = { }, } -function getCliOptionValue(flags) { - for (let index = 0; index < process.argv.length; index += 1) { - const token = process.argv[index] - if (!flags.includes(token)) continue - return process.argv[index + 1] - } - - return undefined -} - -function hasCliFlag(flags) { - return process.argv.some((token) => flags.includes(token)) -} - function shouldPrefixSudo(plan) { return Boolean( plan?.elevated && @@ -171,7 +157,7 @@ async function detectInstalledManagers(filterManager) { } export async function runUpgrade(options) { - const resolvedOptions = typeof options?.opts === 'function' ? options.opts() : options + const resolvedOptions = optsOf(options) const parentOptions = options?.parent?.opts?.() || {} const filterManager = ( resolvedOptions?.manager || diff --git a/src/utils.js b/src/utils.js index 25f3df9..76b0724 100644 --- a/src/utils.js +++ b/src/utils.js @@ -10,6 +10,23 @@ export function isAvailable(cmd) { } } +// Read a flag value straight from argv — commander fallback for nested commands. +export function getCliOptionValue(flags) { + for (let index = 0; index < process.argv.length; index += 1) { + if (flags.includes(process.argv[index])) return process.argv[index + 1] + } + return undefined +} + +export function hasCliFlag(flags) { + return process.argv.some((token) => flags.includes(token)) +} + +// Commander hands the action a Command instance; direct calls pass plain options. +export function optsOf(options) { + return typeof options?.opts === 'function' ? options.opts() : options +} + // 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({ From 2470d1e9652635b1996ac00ce1a88be179150234 Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:12:14 +0700 Subject: [PATCH 07/10] refactor: inline trivial bucket summary and drop dead port regex --- src/ports.js | 3 +-- src/upgrade.js | 9 ++------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/ports.js b/src/ports.js index 7e874c5..b3e043e 100644 --- a/src/ports.js +++ b/src/ports.js @@ -164,8 +164,7 @@ function parseMacPorts(raw) { const pid = Number(parts[1]) const protocol = (parts[7] || 'tcp').toLowerCase() const name = parts.slice(8).join(' ') - const match = - name.match(/(.+)->/) || name.match(/(.+)\(LISTEN\)/) || name.match(/(.+)\(LISTEN\)$/) + const match = name.match(/(.+)->/) || name.match(/(.+)\(LISTEN\)/) const endpoint = (match?.[1] || name).trim() const local = splitAddressPort(endpoint) diff --git a/src/upgrade.js b/src/upgrade.js index 3addcbc..ea19959 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -91,17 +91,12 @@ export function buildUpgradeCommand(manager, plan, packages = []) { return commands.map((command) => `${prefix}${command}`).join(' && ') } -function summarizeResultBucket(result) { - if (result.status === 'success') return 'success' - if (result.status === 'failed') return 'failed' - return 'skipped' -} - function renderUpgradeSummary(results) { const bucketCounts = new Map() for (const result of results) { - const bucket = summarizeResultBucket(result) + const bucket = + result.status === 'success' || result.status === 'failed' ? result.status : 'skipped' bucketCounts.set(bucket, (bucketCounts.get(bucket) || 0) + 1) } From adab8fd5c4b25c6279ca954b8a290c90bdbabdde Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:12:26 +0700 Subject: [PATCH 08/10] build: add Makefile for test/lint/format shortcuts --- Makefile | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8f01081 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: test lint fmt fmt-check verify + +test: ; npm test +lint: ; npm run lint +fmt: ; npm run format +fmt-check: ; npm run format:check +verify: ; npm run format:check && npm run lint && npm test From cc868e1931ec6c1ce6fc17d32c209f83fc8beca9 Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:17:52 +0700 Subject: [PATCH 09/10] chore: ignore local .claude directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7634854..802452b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # macOS .DS_Store +.claude # pkgmap export output pkgmap-export.json From 8ba57c62b09c9497043d98cc2bdf5f60cc65eed4 Mon Sep 17 00:00:00 2001 From: MULHAM Date: Sun, 21 Jun 2026 02:17:52 +0700 Subject: [PATCH 10/10] docs: add AGENTS.md and GEMINI.md agent guides --- AGENTS.md | 1 + GEMINI.md | 1 + 2 files changed, 2 insertions(+) create mode 120000 AGENTS.md create mode 120000 GEMINI.md diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000..bac85c9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +claude.md \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 0000000..bac85c9 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +claude.md \ No newline at end of file