npq 3.23.3 → 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/bin/npq-hero.js +116 -107
- package/bin/npq.js +151 -139
- package/lib/cli.js +26 -8
- package/lib/helpers/auditFailure.js +25 -0
- package/lib/helpers/codingAgentEnvironment.js +23 -0
- package/lib/helpers/jsonOutput.js +24 -0
- package/lib/helpers/jsonPackageSpec.js +29 -0
- package/lib/helpers/jsonReport.js +76 -0
- package/lib/helpers/registryClient.js +2 -7
- package/lib/helpers/registryConfig.js +2 -8
- package/lib/helpers/reportResults.js +5 -16
- package/lib/helpers/versionAge.js +2 -9
- package/lib/jsonAudit.js +71 -0
- package/lib/jsonCli.js +61 -0
- package/lib/marshall.js +11 -6
- package/lib/marshalls/baseMarshall.js +1 -1
- package/lib/marshalls/index.js +44 -7
- package/lib/marshalls/signatures.marshall.js +1 -4
- package/package.json +3 -1
- package/schema/npq-output-v1.schema.json +86 -0
package/lib/cli.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { parseArgs } = require('node:util')
|
|
4
4
|
const npa = require('npm-package-arg')
|
|
5
|
+
const { parseJsonRegistryPackageSpec } = require('./helpers/jsonPackageSpec')
|
|
6
|
+
const { isCodingAgentEnvironment } = require('./helpers/codingAgentEnvironment')
|
|
5
7
|
const pkg = require('../package.json')
|
|
6
8
|
const { parsePackageManagerArguments } = require('./helpers/packageManagerArgumentParser')
|
|
7
9
|
|
|
@@ -45,7 +47,7 @@ class CliParser {
|
|
|
45
47
|
process.exit(exitCode)
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
static _extractPackagesFromPositionals(positionals, earlyExitNoInstall = false) {
|
|
50
|
+
static _extractPackagesFromPositionals(positionals, earlyExitNoInstall = false, json = false) {
|
|
49
51
|
let packages = []
|
|
50
52
|
if (positionals.length > 0) {
|
|
51
53
|
const command = positionals[0]
|
|
@@ -61,7 +63,7 @@ class CliParser {
|
|
|
61
63
|
|
|
62
64
|
// Parse and normalize packages
|
|
63
65
|
return packages.map((pkg) => {
|
|
64
|
-
const parsedPackage = npa(pkg)
|
|
66
|
+
const parsedPackage = json ? parseJsonRegistryPackageSpec(pkg) : npa(pkg)
|
|
65
67
|
const versionModifier = parsedPackage.fetchSpec === '*' ? 'latest' : parsedPackage.fetchSpec
|
|
66
68
|
return `${parsedPackage.name}@${versionModifier}`
|
|
67
69
|
})
|
|
@@ -71,6 +73,7 @@ class CliParser {
|
|
|
71
73
|
const options = {
|
|
72
74
|
'dry-run': { type: 'boolean' },
|
|
73
75
|
plain: { type: 'boolean' },
|
|
76
|
+
json: { type: 'boolean' },
|
|
74
77
|
packageManager: { type: 'string' },
|
|
75
78
|
pkgMgr: { type: 'string' },
|
|
76
79
|
'disable-auto-continue': { type: 'boolean' },
|
|
@@ -102,6 +105,7 @@ Commands:
|
|
|
102
105
|
Options:
|
|
103
106
|
--dry-run Run checks only, don't install
|
|
104
107
|
--plain Force non-rich text output
|
|
108
|
+
--json Emit JSON and never install
|
|
105
109
|
--packageManager Package Manager to use (default: npm)
|
|
106
110
|
--pkgMgr Alias for packageManager
|
|
107
111
|
--disable-auto-continue Disable auto-continue countdown, always prompt
|
|
@@ -129,7 +133,8 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
|
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
// Process install command and packages
|
|
132
|
-
const
|
|
136
|
+
const json = values.json === true || isCodingAgentEnvironment()
|
|
137
|
+
const normalizedPackages = this._extractPackagesFromPositionals(positionals, false, json)
|
|
133
138
|
const installSubcommandExplicit =
|
|
134
139
|
positionals.length > 0 && this.isInstallSubcommand(positionals[0])
|
|
135
140
|
|
|
@@ -138,6 +143,7 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
|
|
|
138
143
|
packageManager: process.env.NPQ_PKG_MGR || values.packageManager || values.pkgMgr || 'npm',
|
|
139
144
|
dryRun: values['dry-run'] || false,
|
|
140
145
|
plain: values.plain || false,
|
|
146
|
+
json,
|
|
141
147
|
disableAutoContinue:
|
|
142
148
|
values['disable-auto-continue'] || process.env.NPQ_DISABLE_AUTO_CONTINUE === 'true',
|
|
143
149
|
registryConfigArgs: this.registryConfigArgs(values),
|
|
@@ -145,19 +151,31 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
|
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
static parseArgsMinimal() {
|
|
154
|
+
static parseArgsMinimal({ codingAgentEnvironment = isCodingAgentEnvironment() } = {}) {
|
|
149
155
|
const packageManager = process.env.NPQ_PKG_MGR || 'npm'
|
|
150
156
|
const { values, positionals } = parsePackageManagerArguments({
|
|
151
157
|
packageManager,
|
|
152
158
|
args: process.argv.slice(2)
|
|
153
159
|
})
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
160
|
+
const installSubcommandExplicit =
|
|
161
|
+
positionals.length > 0 && this.isInstallSubcommand(positionals[0])
|
|
162
|
+
const json = codingAgentEnvironment && installSubcommandExplicit
|
|
163
|
+
let normalizedPackages
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
normalizedPackages = this._extractPackagesFromPositionals(positionals, true, json)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (json && error && typeof error === 'object') {
|
|
169
|
+
Object.defineProperty(error, 'npqJsonMode', { value: true })
|
|
170
|
+
}
|
|
171
|
+
throw error
|
|
172
|
+
}
|
|
157
173
|
|
|
158
174
|
return {
|
|
159
175
|
packages: normalizedPackages,
|
|
160
|
-
registryConfigArgs: this.registryConfigArgs(values)
|
|
176
|
+
registryConfigArgs: this.registryConfigArgs(values),
|
|
177
|
+
installSubcommandExplicit,
|
|
178
|
+
json
|
|
161
179
|
}
|
|
162
180
|
}
|
|
163
181
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const AUDIT_FAILURE_CODES = Object.freeze({
|
|
4
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
5
|
+
PROJECT_MANIFEST_ERROR: 'PROJECT_MANIFEST_ERROR',
|
|
6
|
+
PACKAGE_LOOKUP_FAILED: 'PACKAGE_LOOKUP_FAILED',
|
|
7
|
+
AUDIT_CHECK_FAILED: 'AUDIT_CHECK_FAILED',
|
|
8
|
+
INTERRUPTED: 'INTERRUPTED',
|
|
9
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR'
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const knownCodes = new Set(Object.values(AUDIT_FAILURE_CODES))
|
|
13
|
+
|
|
14
|
+
function createAuditFailure(code, message, context = {}) {
|
|
15
|
+
const failure = {
|
|
16
|
+
code: knownCodes.has(code) ? code : AUDIT_FAILURE_CODES.INTERNAL_ERROR,
|
|
17
|
+
message: String(message)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (context.package) failure.package = String(context.package)
|
|
21
|
+
if (context.marshall) failure.marshall = String(context.marshall)
|
|
22
|
+
return failure
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { AUDIT_FAILURE_CODES, createAuditFailure }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const CODING_AGENT_ENVIRONMENT_VARIABLES = Object.freeze([
|
|
4
|
+
'CLAUDECODE',
|
|
5
|
+
'CLAUDE_CODE_CHILD_SESSION',
|
|
6
|
+
'CURSOR_AGENT',
|
|
7
|
+
'PI_CODING_AGENT',
|
|
8
|
+
'CODEX_SANDBOX',
|
|
9
|
+
'CODEX_THREAD_ID',
|
|
10
|
+
'GEMINI_CLI',
|
|
11
|
+
'WINDSURF_AGENT',
|
|
12
|
+
'CODEIUM_AGENT',
|
|
13
|
+
'AGENT',
|
|
14
|
+
'AI_AGENT'
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
function isCodingAgentEnvironment(env = process.env) {
|
|
18
|
+
return CODING_AGENT_ENVIRONMENT_VARIABLES.some(
|
|
19
|
+
(name) => typeof env[name] === 'string' && env[name].length > 0
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { CODING_AGENT_ENVIRONMENT_VARIABLES, isCodingAgentEnvironment }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function createJsonOutput(write = (value, callback) => process.stdout.write(value, callback)) {
|
|
4
|
+
let written = false
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
write(report, onComplete) {
|
|
8
|
+
if (written) return false
|
|
9
|
+
written = true
|
|
10
|
+
const serialized = `${JSON.stringify(report)}\n`
|
|
11
|
+
if (typeof onComplete === 'function') {
|
|
12
|
+
write(serialized, onComplete)
|
|
13
|
+
} else {
|
|
14
|
+
write(serialized)
|
|
15
|
+
}
|
|
16
|
+
return true
|
|
17
|
+
},
|
|
18
|
+
hasWritten() {
|
|
19
|
+
return written
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { createJsonOutput }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const npa = require('npm-package-arg')
|
|
4
|
+
|
|
5
|
+
const JSON_REGISTRY_SPEC_TYPES = new Set(['tag', 'version', 'range'])
|
|
6
|
+
|
|
7
|
+
function parseJsonRegistryPackageSpec(packageSpec) {
|
|
8
|
+
try {
|
|
9
|
+
const parsedPackage = npa(packageSpec)
|
|
10
|
+
if (!parsedPackage.name || !JSON_REGISTRY_SPEC_TYPES.has(parsedPackage.type)) {
|
|
11
|
+
throw new Error('Unsupported package spec')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return parsedPackage
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error('Invalid JSON package input')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isJsonRegistryPackageSpec(packageSpec) {
|
|
21
|
+
try {
|
|
22
|
+
parseJsonRegistryPackageSpec(packageSpec)
|
|
23
|
+
return true
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { isJsonRegistryPackageSpec, parseJsonRegistryPackageSpec }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const pkg = require('../../package.json')
|
|
4
|
+
const { marshallCategories } = require('../marshalls/constants')
|
|
5
|
+
const { createAuditFailure } = require('./auditFailure')
|
|
6
|
+
|
|
7
|
+
function normalizeMarshallGroup(group = {}) {
|
|
8
|
+
const findings = []
|
|
9
|
+
|
|
10
|
+
for (const value of Object.values(group)) {
|
|
11
|
+
if (!value || typeof value !== 'object') continue
|
|
12
|
+
const category = marshallCategories[value.categoryId] || {
|
|
13
|
+
id: value.categoryId,
|
|
14
|
+
title: value.categoryId
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const warning of value.warnings || []) {
|
|
18
|
+
findings.push({
|
|
19
|
+
severity: 'warning',
|
|
20
|
+
marshall: value.marshall,
|
|
21
|
+
category: { id: category.id, title: category.title },
|
|
22
|
+
message: warning.message
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
for (const error of value.errors || []) {
|
|
26
|
+
findings.push({
|
|
27
|
+
severity: 'error',
|
|
28
|
+
marshall: value.marshall,
|
|
29
|
+
category: { id: category.id, title: category.title },
|
|
30
|
+
message: error.message
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return findings
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildJsonReport({
|
|
39
|
+
packages = [],
|
|
40
|
+
marshallResults = {},
|
|
41
|
+
failures = [],
|
|
42
|
+
version = pkg.version
|
|
43
|
+
} = {}) {
|
|
44
|
+
const positionalResults = Array.isArray(marshallResults)
|
|
45
|
+
const packageReports = packages.map((requested, index) => {
|
|
46
|
+
const results = positionalResults ? marshallResults[index] : marshallResults[requested]
|
|
47
|
+
return {
|
|
48
|
+
requested,
|
|
49
|
+
findings: (results || []).flatMap(normalizeMarshallGroup)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
const findings = packageReports.flatMap((entry) => entry.findings)
|
|
53
|
+
const errors = findings.filter((item) => item.severity === 'error').length
|
|
54
|
+
const warnings = findings.filter((item) => item.severity === 'warning').length
|
|
55
|
+
const status = failures.length > 0 ? 'failed' : errors + warnings > 0 ? 'findings' : 'clean'
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
tool: { name: 'npq', version },
|
|
60
|
+
status,
|
|
61
|
+
summary: { packagesAudited: packageReports.length, errors, warnings },
|
|
62
|
+
packages: packageReports,
|
|
63
|
+
failures: failures.map((failure) =>
|
|
64
|
+
createAuditFailure(failure.code, failure.message, {
|
|
65
|
+
package: failure.package,
|
|
66
|
+
marshall: failure.marshall
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function exitCodeForJsonReport(report) {
|
|
73
|
+
return report.status === 'failed' ? 2 : report.status === 'findings' ? 1 : 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { buildJsonReport, exitCodeForJsonReport }
|
|
@@ -16,8 +16,7 @@ const CAPABILITY = Object.freeze({
|
|
|
16
16
|
const CAPABILITY_MESSAGE = Object.freeze({
|
|
17
17
|
[CAPABILITY.SIGNING_KEYS]: 'configured registry does not expose signing keys',
|
|
18
18
|
[CAPABILITY.ATTESTATIONS]: 'configured registry does not expose attestations',
|
|
19
|
-
[CAPABILITY.DOWNLOAD_COUNTS]:
|
|
20
|
-
'download counts are available only for the public npm registry'
|
|
19
|
+
[CAPABILITY.DOWNLOAD_COUNTS]: 'download counts are available only for the public npm registry'
|
|
21
20
|
})
|
|
22
21
|
|
|
23
22
|
class RegistryClient {
|
|
@@ -47,11 +46,7 @@ class RegistryClient {
|
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
async requestJson(
|
|
51
|
-
requestPath,
|
|
52
|
-
packageSpec,
|
|
53
|
-
{ capability = null, notFoundAsData = false } = {}
|
|
54
|
-
) {
|
|
49
|
+
async requestJson(requestPath, packageSpec, { capability = null, notFoundAsData = false } = {}) {
|
|
55
50
|
const registry = this.registryFor(packageSpec)
|
|
56
51
|
try {
|
|
57
52
|
return await this.fetcher.json(requestPath, this.requestOptions(packageSpec))
|
|
@@ -3,12 +3,7 @@
|
|
|
3
3
|
const path = require('node:path')
|
|
4
4
|
const Config = require('@npmcli/config')
|
|
5
5
|
const npmFetch = require('npm-registry-fetch')
|
|
6
|
-
const {
|
|
7
|
-
definitions,
|
|
8
|
-
flatten,
|
|
9
|
-
nerfDarts,
|
|
10
|
-
shorthands
|
|
11
|
-
} = require('@npmcli/config/lib/definitions')
|
|
6
|
+
const { definitions, flatten, nerfDarts, shorthands } = require('@npmcli/config/lib/definitions')
|
|
12
7
|
const { RegistryError, sanitizeRegistryUrl } = require('./registryErrors')
|
|
13
8
|
|
|
14
9
|
const DEFAULT_REGISTRY = 'https://registry.npmjs.org/'
|
|
@@ -57,8 +52,7 @@ class RegistryConfig {
|
|
|
57
52
|
}
|
|
58
53
|
return new RegistryConfig(config.flat)
|
|
59
54
|
} catch (error) {
|
|
60
|
-
const code =
|
|
61
|
-
error.code === 'ERR_INVALID_AUTH' ? 'EREGISTRYCONFIGAUTH' : 'EREGISTRYCONFIG'
|
|
55
|
+
const code = error.code === 'ERR_INVALID_AUTH' ? 'EREGISTRYCONFIGAUTH' : 'EREGISTRYCONFIG'
|
|
62
56
|
const message =
|
|
63
57
|
error.code === 'ERR_INVALID_AUTH'
|
|
64
58
|
? 'Invalid npm registry authentication configuration'
|
|
@@ -230,15 +230,9 @@ function reportResults(marshallResults, { plain = false } = {}) {
|
|
|
230
230
|
entry.categoryFriendlyName.padEnd(maxTextLength, ' ')
|
|
231
231
|
)
|
|
232
232
|
const iconRich = styleText(['cyan'], '○')
|
|
233
|
-
const wrappedMessage = wrapTextWithIndent(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
indentString
|
|
237
|
-
)
|
|
238
|
-
resultsForPrettyPrint +=
|
|
239
|
-
`\n ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
|
|
240
|
-
resultsForPlainTextPrint +=
|
|
241
|
-
`\n NOT EVALUATED: ${entry.categoryFriendlyName} - ${entry.message}`
|
|
233
|
+
const wrappedMessage = wrapTextWithIndent(entry.message, maxMessageWidth, indentString)
|
|
234
|
+
resultsForPrettyPrint += `\n ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
|
|
235
|
+
resultsForPlainTextPrint += `\n NOT EVALUATED: ${entry.categoryFriendlyName} - ${entry.message}`
|
|
242
236
|
})
|
|
243
237
|
})
|
|
244
238
|
}
|
|
@@ -256,16 +250,11 @@ function reportResults(marshallResults, { plain = false } = {}) {
|
|
|
256
250
|
terminalWidth - getDisplayLength(' │ ℹ Suggested older release · ') - 2
|
|
257
251
|
)
|
|
258
252
|
const detailWidth = Math.max(20, terminalWidth - getDisplayLength(' │ ') - 2)
|
|
259
|
-
const wrappedHeadline = wrapTextWithIndent(
|
|
260
|
-
headline,
|
|
261
|
-
headlineWidth,
|
|
262
|
-
suggestionIndent
|
|
263
|
-
)
|
|
253
|
+
const wrappedHeadline = wrapTextWithIndent(headline, headlineWidth, suggestionIndent)
|
|
264
254
|
const wrappedDetail = wrapTextWithIndent(detail, detailWidth, suggestionIndent)
|
|
265
255
|
|
|
266
256
|
resultsForPrettyPrint += `\n ${prefixRich}`
|
|
267
|
-
resultsForPrettyPrint +=
|
|
268
|
-
`\n ${prefixRich} ${iconRich} ${labelRich} ${separatorRich} ${wrappedHeadline}`
|
|
257
|
+
resultsForPrettyPrint += `\n ${prefixRich} ${iconRich} ${labelRich} ${separatorRich} ${wrappedHeadline}`
|
|
269
258
|
resultsForPrettyPrint += `\n${suggestionIndent}${wrappedDetail}`
|
|
270
259
|
|
|
271
260
|
resultsForPlainTextPrint +=
|
|
@@ -23,12 +23,7 @@ function findAlternativeVersion({
|
|
|
23
23
|
requestedVersion,
|
|
24
24
|
now = Date.now()
|
|
25
25
|
}) {
|
|
26
|
-
if (
|
|
27
|
-
!packageInfo ||
|
|
28
|
-
!packageInfo.versions ||
|
|
29
|
-
!packageInfo.time ||
|
|
30
|
-
!semver.valid(targetVersion)
|
|
31
|
-
) {
|
|
26
|
+
if (!packageInfo || !packageInfo.versions || !packageInfo.time || !semver.valid(targetVersion)) {
|
|
32
27
|
return null
|
|
33
28
|
}
|
|
34
29
|
|
|
@@ -49,9 +44,7 @@ function findAlternativeVersion({
|
|
|
49
44
|
})
|
|
50
45
|
.filter(
|
|
51
46
|
({ publishedAt, ageDays }) =>
|
|
52
|
-
typeof publishedAt === 'string' &&
|
|
53
|
-
ageDays !== null &&
|
|
54
|
-
ageDays > MAX_VERSION_RECENCY_DAYS
|
|
47
|
+
typeof publishedAt === 'string' && ageDays !== null && ageDays > MAX_VERSION_RECENCY_DAYS
|
|
55
48
|
)
|
|
56
49
|
.sort((left, right) => semver.rcompare(left.version, right.version))
|
|
57
50
|
|
package/lib/jsonAudit.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { getProjectPackages } = require('./helpers/sourcePackages')
|
|
4
|
+
const Marshall = require('./marshall')
|
|
5
|
+
const { promiseThrottleHelper } = require('./helpers/promiseThrottler')
|
|
6
|
+
const { buildJsonReport } = require('./helpers/jsonReport')
|
|
7
|
+
const { AUDIT_FAILURE_CODES, createAuditFailure } = require('./helpers/auditFailure')
|
|
8
|
+
const { isJsonRegistryPackageSpec } = require('./helpers/jsonPackageSpec')
|
|
9
|
+
const RegistryConfig = require('./helpers/registryConfig')
|
|
10
|
+
const RegistryClient = require('./helpers/registryClient')
|
|
11
|
+
|
|
12
|
+
async function runJsonAudit(cliArgs, dependencies = {}) {
|
|
13
|
+
const getPackages = dependencies.getProjectPackages || getProjectPackages
|
|
14
|
+
const MarshallClass = dependencies.Marshall || Marshall
|
|
15
|
+
const throttle = dependencies.promiseThrottleHelper || promiseThrottleHelper
|
|
16
|
+
const RegistryConfigClass = dependencies.RegistryConfig || RegistryConfig
|
|
17
|
+
const RegistryClientClass = dependencies.RegistryClient || RegistryClient
|
|
18
|
+
let packages = cliArgs.packages || []
|
|
19
|
+
const failures = []
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (packages.length === 0) {
|
|
23
|
+
const projectPackages = await getPackages()
|
|
24
|
+
if (projectPackages && projectPackages.error) {
|
|
25
|
+
return buildJsonReport({
|
|
26
|
+
failures: [
|
|
27
|
+
createAuditFailure(
|
|
28
|
+
AUDIT_FAILURE_CODES.PROJECT_MANIFEST_ERROR,
|
|
29
|
+
'Unable to read project package.json'
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
packages = projectPackages
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!packages.every(isJsonRegistryPackageSpec)) {
|
|
38
|
+
return buildJsonReport({
|
|
39
|
+
failures: [
|
|
40
|
+
createAuditFailure(
|
|
41
|
+
AUDIT_FAILURE_CODES.INVALID_INPUT,
|
|
42
|
+
'Invalid package or option argument'
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const registryConfig = await RegistryConfigClass.load({
|
|
49
|
+
argv: cliArgs.registryConfigArgs || []
|
|
50
|
+
})
|
|
51
|
+
const registryClient = new RegistryClientClass(registryConfig)
|
|
52
|
+
const marshall = new MarshallClass({
|
|
53
|
+
pkgs: packages,
|
|
54
|
+
progressManager: null,
|
|
55
|
+
promiseThrottleHelper: throttle,
|
|
56
|
+
registryClient,
|
|
57
|
+
preserveRequestOrder: true,
|
|
58
|
+
suppressOutput: true,
|
|
59
|
+
onAuditFailure: (failure) => failures.push(failure)
|
|
60
|
+
})
|
|
61
|
+
const marshallResults = await marshall.process()
|
|
62
|
+
return buildJsonReport({ packages, marshallResults, failures })
|
|
63
|
+
} catch {
|
|
64
|
+
failures.push(
|
|
65
|
+
createAuditFailure(AUDIT_FAILURE_CODES.INTERNAL_ERROR, 'JSON audit could not complete')
|
|
66
|
+
)
|
|
67
|
+
return buildJsonReport({ packages, failures })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { runJsonAudit }
|
package/lib/jsonCli.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createJsonOutput } = require('./helpers/jsonOutput')
|
|
4
|
+
const { buildJsonReport, exitCodeForJsonReport } = require('./helpers/jsonReport')
|
|
5
|
+
const { AUDIT_FAILURE_CODES, createAuditFailure } = require('./helpers/auditFailure')
|
|
6
|
+
const { runJsonAudit } = require('./jsonAudit')
|
|
7
|
+
|
|
8
|
+
function writeInvalidJsonInvocation(output = createJsonOutput()) {
|
|
9
|
+
const report = buildJsonReport({
|
|
10
|
+
failures: [
|
|
11
|
+
createAuditFailure(AUDIT_FAILURE_CODES.INVALID_INPUT, 'Invalid package or option argument')
|
|
12
|
+
]
|
|
13
|
+
})
|
|
14
|
+
output.write(report)
|
|
15
|
+
return report
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function runJsonCli(cliArgs, dependencies = {}) {
|
|
19
|
+
const output = dependencies.output || createJsonOutput()
|
|
20
|
+
const processTarget = dependencies.processTarget || process
|
|
21
|
+
const audit = dependencies.runJsonAudit || runJsonAudit
|
|
22
|
+
const getExitCode = dependencies.exitCodeForJsonReport || exitCodeForJsonReport
|
|
23
|
+
let interrupted = false
|
|
24
|
+
let interruptedReport
|
|
25
|
+
|
|
26
|
+
const interrupt = () => {
|
|
27
|
+
interrupted = true
|
|
28
|
+
interruptedReport = buildJsonReport({
|
|
29
|
+
packages: cliArgs.packages || [],
|
|
30
|
+
failures: [createAuditFailure(AUDIT_FAILURE_CODES.INTERRUPTED, 'Audit interrupted')]
|
|
31
|
+
})
|
|
32
|
+
output.write(interruptedReport, () => {
|
|
33
|
+
processTarget.exit(2)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
processTarget.once('SIGINT', interrupt)
|
|
38
|
+
let report
|
|
39
|
+
try {
|
|
40
|
+
report = await audit(cliArgs)
|
|
41
|
+
} catch {
|
|
42
|
+
report = buildJsonReport({
|
|
43
|
+
packages: cliArgs.packages || [],
|
|
44
|
+
failures: [
|
|
45
|
+
createAuditFailure(AUDIT_FAILURE_CODES.INTERNAL_ERROR, 'JSON audit could not complete')
|
|
46
|
+
]
|
|
47
|
+
})
|
|
48
|
+
} finally {
|
|
49
|
+
processTarget.removeListener('SIGINT', interrupt)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (interrupted) {
|
|
53
|
+
return interruptedReport
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
output.write(report)
|
|
57
|
+
processTarget.exitCode = getExitCode(report)
|
|
58
|
+
return report
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { runJsonCli, writeInvalidJsonInvocation }
|
package/lib/marshall.js
CHANGED
|
@@ -11,14 +11,15 @@ class Marshall {
|
|
|
11
11
|
options = options || {}
|
|
12
12
|
this.pkgs = options.pkgs || null
|
|
13
13
|
this.registryClient =
|
|
14
|
-
options.registryClient ||
|
|
15
|
-
options.packageRepoUtils?.registryClient ||
|
|
16
|
-
RegistryClient.public()
|
|
14
|
+
options.registryClient || options.packageRepoUtils?.registryClient || RegistryClient.public()
|
|
17
15
|
this.packageRepoUtils =
|
|
18
|
-
options.packageRepoUtils ||
|
|
19
|
-
new PackageRepoUtils({ registryClient: this.registryClient })
|
|
16
|
+
options.packageRepoUtils || new PackageRepoUtils({ registryClient: this.registryClient })
|
|
20
17
|
this.progressManager = options.progressManager || null
|
|
21
18
|
this.promiseThrottleHelper = options.promiseThrottleHelper || null
|
|
19
|
+
this.preserveRequestOrder = options.preserveRequestOrder === true
|
|
20
|
+
this.suppressOutput = options.suppressOutput === true
|
|
21
|
+
this.onAuditFailure =
|
|
22
|
+
typeof options.onAuditFailure === 'function' ? options.onAuditFailure : null
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async process() {
|
|
@@ -57,6 +58,7 @@ class Marshall {
|
|
|
57
58
|
this.progressManager.stop()
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
if (this.preserveRequestOrder) return res
|
|
60
62
|
return promiseResultsPerPackage
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -67,7 +69,10 @@ class Marshall {
|
|
|
67
69
|
packageRepoUtils: packageRepoUtils,
|
|
68
70
|
registryClient: this.registryClient
|
|
69
71
|
}
|
|
70
|
-
return Marshalls.tasks(config, this.progressManager
|
|
72
|
+
return Marshalls.tasks(config, this.progressManager, {
|
|
73
|
+
onAuditFailure: this.onAuditFailure,
|
|
74
|
+
suppressOutput: this.suppressOutput
|
|
75
|
+
})
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
createPackageVersionMaps(packages) {
|
|
@@ -9,7 +9,7 @@ const { marshallCategories } = require('./constants')
|
|
|
9
9
|
|
|
10
10
|
class BaseMarshall {
|
|
11
11
|
constructor(options) {
|
|
12
|
-
this.debug = util.debuglog('npq')
|
|
12
|
+
this.debug = options.suppressOutput ? () => {} : util.debuglog('npq')
|
|
13
13
|
this.packageRepoUtils = options.packageRepoUtils
|
|
14
14
|
this.registryClient = options.registryClient
|
|
15
15
|
this.categoryId = marshallCategories.PackageHealth.id
|
package/lib/marshalls/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs')
|
|
4
4
|
const path = require('node:path')
|
|
5
|
+
const { AUDIT_FAILURE_CODES, createAuditFailure } = require('../helpers/auditFailure')
|
|
5
6
|
const { RegistryError } = require('../helpers/registryErrors')
|
|
6
7
|
|
|
7
8
|
class Marshalls {
|
|
@@ -22,7 +23,8 @@ class Marshalls {
|
|
|
22
23
|
|
|
23
24
|
const config = {
|
|
24
25
|
packageRepoUtils: options.packageRepoUtils,
|
|
25
|
-
registryClient: options.registryClient
|
|
26
|
+
registryClient: options.registryClient,
|
|
27
|
+
suppressOutput: options.suppressOutput === true
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const marshallTasks = marshalls.reduce((prev, curr) => {
|
|
@@ -36,6 +38,7 @@ class Marshalls {
|
|
|
36
38
|
|
|
37
39
|
return prev.concat({
|
|
38
40
|
title: marshall.title(),
|
|
41
|
+
name: marshall.name,
|
|
39
42
|
categoryId: marshall.categoryId,
|
|
40
43
|
execute: async (options) => {
|
|
41
44
|
const ctx = {
|
|
@@ -61,7 +64,11 @@ class Marshalls {
|
|
|
61
64
|
* @param {Object} [progressManager] - Optional progress manager for displaying audit progress updates
|
|
62
65
|
* @returns {Promise<Array<Object>>} Array of marshall results containing audit findings for each package
|
|
63
66
|
*/
|
|
64
|
-
static async tasks(
|
|
67
|
+
static async tasks(
|
|
68
|
+
options,
|
|
69
|
+
progressManager,
|
|
70
|
+
{ onAuditFailure = null, suppressOutput = false } = {}
|
|
71
|
+
) {
|
|
65
72
|
const marshallResults = []
|
|
66
73
|
|
|
67
74
|
// @TODO handle throw error because fetch didn't work due to network issues
|
|
@@ -78,7 +85,23 @@ class Marshalls {
|
|
|
78
85
|
// hostname: 'registry.npmjs.org'
|
|
79
86
|
// }
|
|
80
87
|
// }
|
|
81
|
-
|
|
88
|
+
let packagesDataList
|
|
89
|
+
try {
|
|
90
|
+
packagesDataList = await Marshalls.warmUpPackagesCache(options)
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (!onAuditFailure) throw error
|
|
93
|
+
|
|
94
|
+
for (const pkg of options.pkgs) {
|
|
95
|
+
onAuditFailure(
|
|
96
|
+
createAuditFailure(
|
|
97
|
+
AUDIT_FAILURE_CODES.PACKAGE_LOOKUP_FAILED,
|
|
98
|
+
'Unable to retrieve package metadata',
|
|
99
|
+
{ package: pkg.packageString }
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
return marshallResults
|
|
104
|
+
}
|
|
82
105
|
|
|
83
106
|
// handle error in case we get just one package and it is not found
|
|
84
107
|
// if (packagesDataList && packagesDataList.length === 1) {
|
|
@@ -123,7 +146,8 @@ class Marshalls {
|
|
|
123
146
|
if (packagesToProcess.length > 0) {
|
|
124
147
|
const marshallTasks = await Marshalls.buildMarshallTasks(marshalls, {
|
|
125
148
|
packageRepoUtils: options.packageRepoUtils,
|
|
126
|
-
registryClient: options.registryClient
|
|
149
|
+
registryClient: options.registryClient,
|
|
150
|
+
suppressOutput
|
|
127
151
|
})
|
|
128
152
|
|
|
129
153
|
for (const marshall of marshallTasks) {
|
|
@@ -134,10 +158,23 @@ class Marshalls {
|
|
|
134
158
|
})
|
|
135
159
|
marshallResults.push(res.marshalls)
|
|
136
160
|
} catch (error) {
|
|
137
|
-
if (
|
|
138
|
-
|
|
161
|
+
if (!onAuditFailure) {
|
|
162
|
+
if (error instanceof RegistryError) {
|
|
163
|
+
throw error
|
|
164
|
+
}
|
|
165
|
+
console.error(`Error running task ${marshall.title}:`, error)
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const pkg of options.pkgs) {
|
|
170
|
+
onAuditFailure(
|
|
171
|
+
createAuditFailure(
|
|
172
|
+
AUDIT_FAILURE_CODES.AUDIT_CHECK_FAILED,
|
|
173
|
+
'Audit check could not complete',
|
|
174
|
+
{ package: pkg.packageString, marshall: marshall.name }
|
|
175
|
+
)
|
|
176
|
+
)
|
|
139
177
|
}
|
|
140
|
-
console.error(`Error running task ${marshall.title}:`, error)
|
|
141
178
|
}
|
|
142
179
|
}
|
|
143
180
|
}
|