npq 3.8.1 → 3.9.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.
Files changed (48) hide show
  1. package/.github/npq.mov +0 -0
  2. package/.github/npq.mp4 +0 -0
  3. package/.github/npq.png +0 -0
  4. package/.github/workflows/main.yml +2 -2
  5. package/README.md +18 -9
  6. package/__tests__/__fixtures__/test.marshall.js +5 -6
  7. package/__tests__/cli.test.js +110 -0
  8. package/__tests__/cliPrompt.test.js +409 -0
  9. package/__tests__/marshalls.base.test.js +1 -1
  10. package/__tests__/marshalls.classMethods.test.js +11 -14
  11. package/__tests__/marshalls.expiredDomains.test.js +2 -2
  12. package/__tests__/marshalls.provenance.test.js +181 -4
  13. package/__tests__/marshalls.signatures.test.js +0 -4
  14. package/__tests__/marshalls.snyk.test.js +294 -0
  15. package/__tests__/marshalls.tasks.test.js +48 -24
  16. package/__tests__/marshalls.typosquatting.test.js +16 -9
  17. package/__tests__/reportResults.test.js +772 -0
  18. package/__tests__/scripts.test.js +2 -2
  19. package/bin/npq-hero.js +70 -20
  20. package/bin/npq.js +113 -21
  21. package/lib/cli.js +127 -24
  22. package/lib/helpers/cliPrompt.js +97 -0
  23. package/lib/helpers/cliSpinner.js +104 -0
  24. package/lib/helpers/cliSupportHandler.js +50 -7
  25. package/lib/helpers/packageRepoUtils.js +5 -0
  26. package/lib/helpers/promiseThrottler.js +96 -0
  27. package/lib/helpers/reportResults.js +304 -0
  28. package/lib/helpers/sourcePackages.js +36 -0
  29. package/lib/marshall.js +42 -65
  30. package/lib/marshalls/age.marshall.js +3 -2
  31. package/lib/marshalls/author.marshall.js +36 -7
  32. package/lib/marshalls/baseMarshall.js +9 -17
  33. package/lib/marshalls/deprecation.marshall.js +43 -0
  34. package/lib/marshalls/downloads.marshall.js +1 -1
  35. package/lib/marshalls/expiredDomains.marshall.js +7 -5
  36. package/lib/marshalls/index.js +110 -80
  37. package/lib/marshalls/license.marshall.js +1 -1
  38. package/lib/marshalls/newbin.marshall.js +1 -2
  39. package/lib/marshalls/provenance.marshall.js +152 -33
  40. package/lib/marshalls/repo.marshall.js +6 -5
  41. package/lib/marshalls/scripts.marshall.js +1 -1
  42. package/lib/marshalls/signatures.marshall.js +26 -2
  43. package/lib/marshalls/snyk.marshall.js +61 -15
  44. package/lib/marshalls/typosquatting.marshall.js +8 -5
  45. package/lib/marshalls/version-maturity.marshall.js +1 -1
  46. package/package.json +16 -19
  47. package/scripts/postinstall.js +10 -11
  48. package/lib/cliCommons.js +0 -80
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const semver = require('semver')
3
4
  const BaseMarshall = require('./baseMarshall')
4
5
  const pacote = require('pacote')
5
6
  const Warning = require('../helpers/warning')
@@ -21,48 +22,166 @@ class Marshall extends BaseMarshall {
21
22
  validate(pkg) {
22
23
  const validationMetadata = {}
23
24
 
24
- return this.packageRepoUtils
25
- .getPackageInfo(pkg.packageName)
26
- .then((packageInfo) => {
27
- const packageName = packageInfo.name
28
- const packageVersion =
29
- pkg.packageVersion === 'latest'
30
- ? packageInfo['dist-tags'] && packageInfo['dist-tags'].latest
31
- : this.packageRepoUtils.parsePackageVersion(pkg.packageVersion).version
25
+ return (
26
+ this.packageRepoUtils
27
+ .getPackageInfo(pkg.packageName)
28
+ .then((packageInfo) => {
29
+ const packageName = packageInfo.name
30
+ const packageVersion =
31
+ pkg.packageVersion === 'latest'
32
+ ? packageInfo['dist-tags'] && packageInfo['dist-tags'].latest
33
+ : this.packageRepoUtils.parsePackageVersion(pkg.packageVersion).version
32
34
 
33
- validationMetadata.name = packageName
34
- validationMetadata.version = packageVersion
35
+ validationMetadata.name = packageName
36
+ validationMetadata.version = packageVersion
37
+ validationMetadata.packageInfo = packageInfo
35
38
 
36
- if (!validationMetadata.version) {
37
- throw new Error('Unable to find version or dist-tag for package')
38
- }
39
- })
40
- .then(() => {
41
- return this.fetchRegistryKeys()
42
- })
43
- .then((keys) => {
44
- // @TODO currently we're hardcoding the official npm registry
45
- // this should however allow for local proxies and other registries
46
- return pacote.manifest(`${validationMetadata.name}@${validationMetadata.version}`, {
39
+ if (!validationMetadata.version) {
40
+ throw new Error('Unable to find version or dist-tag for package')
41
+ }
42
+ })
43
+ // Disabled for now, see notes in function due to slow performance for
44
+ // running this check
45
+ // .then(() => {
46
+ // return this.checkProvenanceRegression(validationMetadata)
47
+ // })
48
+ .then(() => {
49
+ return this.fetchRegistryKeys()
50
+ })
51
+ .then((keys) => {
52
+ // @TODO currently we're hardcoding the official npm registry
53
+ // this should however allow for local proxies and other registries
54
+ return pacote.manifest(`${validationMetadata.name}@${validationMetadata.version}`, {
55
+ verifyAttestations: true,
56
+ registry: 'https://registry.npmjs.org',
57
+
58
+ '//registry.npmjs.org/:_keys': keys
59
+ })
60
+ })
61
+ .then((metadata) => {
62
+ if (!metadata || !metadata._attestations) {
63
+ throw new Warning('the package was published without any attestations')
64
+ }
65
+
66
+ const attestations = metadata._attestations
67
+
68
+ return attestations
69
+ })
70
+ .catch((error) => {
71
+ // We can ignore this type of error, false positive
72
+ // See: https://github.com/lirantal/npq/issues/329
73
+ if (
74
+ error.code === 'EATTESTATIONVERIFY' &&
75
+ error.message.includes('malformed checkpoint')
76
+ ) {
77
+ return []
78
+ } else {
79
+ if (error.message.includes('package was published without any attestations')) {
80
+ throw new Warning(`Unable to verify provenance: ${error.message}`)
81
+ } else {
82
+ this.debug(
83
+ '\nUnable to verify provenance for package %s@%s: %s',
84
+ validationMetadata.name,
85
+ validationMetadata.version,
86
+ error.message
87
+ )
88
+ throw new Warning(`Unable to verify provenance`)
89
+ }
90
+ }
91
+ })
92
+ )
93
+ }
94
+
95
+ /*
96
+ async checkProvenanceRegression(validationMetadata) {
97
+ const { packageInfo, name, version } = validationMetadata
98
+
99
+ if (!packageInfo || !packageInfo.versions) {
100
+ return // Cannot check regression without version information
101
+ }
102
+
103
+ const allVersions = Object.keys(packageInfo.versions)
104
+ const validVersions = allVersions.filter(v => semver.valid(v))
105
+
106
+ if (validVersions.length === 0) {
107
+ return // No valid versions to compare
108
+ }
109
+
110
+ // Get all versions older than the target version
111
+ const olderVersions = validVersions
112
+ .filter(v => semver.lt(v, version))
113
+ .sort(semver.rcompare) // Sort descending, newest first
114
+
115
+ if (olderVersions.length === 0) {
116
+ return // No older versions to check
117
+ }
118
+
119
+ // Check if any older versions had provenance
120
+ let hasProvenanceRegressionDetected = false
121
+ const versionWithProvenance = []
122
+
123
+ // NOTE: Going back all versions and fetching their manifest to check provenance
124
+ // is going to be very slow. We can limit this function to go back just 2-3 versions
125
+ // and then stop there
126
+ for (const olderVersion of olderVersions) {
127
+ try {
128
+ const keys = await this.fetchRegistryKeys()
129
+ const metadata = await pacote.manifest(`${name}@${olderVersion}`, {
47
130
  verifyAttestations: true,
48
131
  registry: 'https://registry.npmjs.org',
49
-
50
132
  '//registry.npmjs.org/:_keys': keys
51
133
  })
52
- })
53
- .then((metadata) => {
54
- if (!metadata || !metadata._attestations) {
55
- throw new Warning('the package was published without any attestations.')
134
+
135
+ if (metadata && metadata._attestations) {
136
+ versionWithProvenance.push(olderVersion)
137
+ hasProvenanceRegressionDetected = true
56
138
  }
139
+ } catch (error) {
140
+ // Ignore errors for older versions as they might not have provenance
141
+ // We only care if they successfully had provenance before
142
+ continue
143
+ }
144
+ }
57
145
 
58
- const attestations = metadata._attestations
146
+ // If older versions had provenance, check if current version lacks it
147
+ if (hasProvenanceRegressionDetected) {
148
+ try {
149
+ const keys = await this.fetchRegistryKeys()
150
+ const currentMetadata = await pacote.manifest(`${name}@${version}`, {
151
+ verifyAttestations: true,
152
+ registry: 'https://registry.npmjs.org',
153
+ '//registry.npmjs.org/:_keys': keys
154
+ })
59
155
 
60
- return attestations
61
- })
62
- .catch((error) => {
63
- throw new Error(`Unable to verify provenance: ${error.message}`)
64
- })
156
+ if (!currentMetadata || !currentMetadata._attestations) {
157
+ const latestVersionWithProvenance = versionWithProvenance[0] // First is the newest
158
+ throw new Error(
159
+ `Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
160
+ )
161
+ }
162
+ } catch (error) {
163
+ // If it's our custom error, re-throw it
164
+ if (error.message.includes('Provenance regression detected')) {
165
+ throw error
166
+ }
167
+
168
+ // If it's a verification error, the current version lacks valid provenance
169
+ if (error.code === 'EATTESTATIONVERIFY' || error.message.includes('attestations')) {
170
+ const latestVersionWithProvenance = versionWithProvenance[0]
171
+ throw new Error(
172
+ `Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
173
+ )
174
+ }
175
+
176
+ // For other errors, treat as if current version lacks provenance
177
+ const latestVersionWithProvenance = versionWithProvenance[0]
178
+ throw new Error(
179
+ `Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
180
+ )
181
+ }
182
+ }
65
183
  }
184
+ */
66
185
 
67
186
  fetchRegistryKeys() {
68
187
  const registryHost = 'https://registry.npmjs.org'
@@ -86,7 +205,7 @@ class Marshall extends BaseMarshall {
86
205
  return keys
87
206
  })
88
207
  .catch((error) => {
89
- throw new Error(`Error fetching registry keys: ${error.message}`)
208
+ throw new Warning(`Error fetching registry keys: ${error.message}`)
90
209
  })
91
210
  }
92
211
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const BaseMarshall = require('./baseMarshall')
4
+ const Warning = require('../helpers/warning')
4
5
  const { marshallCategories } = require('./constants')
5
6
  const URL = require('url').URL
6
7
  const MARSHALL_NAME = 'repo'
@@ -9,7 +10,7 @@ class Marshall extends BaseMarshall {
9
10
  constructor(options) {
10
11
  super(options)
11
12
  this.name = MARSHALL_NAME
12
- this.categoryId = marshallCategories.MalwareDetection.id
13
+ this.categoryId = marshallCategories.PackageHealth.id
13
14
  }
14
15
 
15
16
  title() {
@@ -30,21 +31,21 @@ class Marshall extends BaseMarshall {
30
31
  urlStructure = new URL(lastVersionData.repository.url)
31
32
  urlOfGitRepository = new URL(`https://${urlStructure.host}${urlStructure.pathname}`)
32
33
  } catch (error) {
33
- throw new Error('No valid repository is associated with the package')
34
+ throw new Warning('No valid repository is associated with the package')
34
35
  }
35
36
  return fetch(urlOfGitRepository.href).catch(() => {
36
- throw new Error(
37
+ throw new Warning(
37
38
  `The repository associated with the package (${urlOfGitRepository.href}) does not exist or is unreachable at the moment.`
38
39
  )
39
40
  })
40
41
  } else if (lastVersionData && lastVersionData.homepage) {
41
42
  return fetch(lastVersionData.homepage).catch(() => {
42
- throw new Error(
43
+ throw new Warning(
43
44
  `The homepage associated with the package (${lastVersionData.homepage}) does not exist or is unreachable at the moment.`
44
45
  )
45
46
  })
46
47
  } else {
47
- throw new Error('The package has no associated repository or homepage.')
48
+ throw new Warning('The package has no associated repository or homepage.')
48
49
  }
49
50
  })
50
51
  }
@@ -43,7 +43,7 @@ class Marshall extends BaseMarshall {
43
43
  packageScripts[scriptName].length > 0
44
44
  ) {
45
45
  throw new Error(
46
- `Detected a possible malicious intent script, act carefully: ${scriptName}: ${packageScripts[scriptName]}`
46
+ `Detected a possible malicious intent script, audit required: ${scriptName}: ${packageScripts[scriptName]}`
47
47
  )
48
48
  }
49
49
  })
@@ -1,11 +1,15 @@
1
1
  'use strict'
2
2
 
3
3
  const BaseMarshall = require('./baseMarshall')
4
+ const Warning = require('../helpers/warning')
5
+ const util = require('util')
4
6
  const pacote = require('pacote')
5
7
  const { marshallCategories } = require('./constants')
6
8
 
7
9
  const MARSHALL_NAME = 'signatures'
8
10
 
11
+ let registryKeysCache = null
12
+
9
13
  class Marshall extends BaseMarshall {
10
14
  constructor(options) {
11
15
  super(options)
@@ -20,6 +24,9 @@ class Marshall extends BaseMarshall {
20
24
  validate(pkg) {
21
25
  // @TODO currently we're hardcoding the official npm registry
22
26
  // this should however allow for local proxies and other registries
27
+ // @TODO performance improvement: we should cache the registry keys
28
+ // and not fetch them every time because they don't change between
29
+ // requests
23
30
  return this.fetchRegistryKeys()
24
31
  .then((keys) => {
25
32
  return pacote.manifest(`${pkg.packageName}@${pkg.packageVersion}`, {
@@ -33,11 +40,26 @@ class Marshall extends BaseMarshall {
33
40
  return metadata
34
41
  })
35
42
  .catch((error) => {
36
- throw new Error(`Unable to grab package manifest: ${error.message}`)
43
+ // @TODO error.message that useful for rich debugging when enabled
44
+
45
+ if (
46
+ error.message &&
47
+ error.message.includes('but the corresponding public key has expired')
48
+ ) {
49
+ // if the error is about an expired key, we throw a warning
50
+ // instead of an error, so that the process can continue
51
+ throw new Warning(`Package is signed with an expired key`)
52
+ }
53
+
54
+ throw new Warning(`Unable to verify package signature on registry: ${error.message}`)
37
55
  })
38
56
  }
39
57
 
40
58
  fetchRegistryKeys() {
59
+ if (registryKeysCache) {
60
+ return Promise.resolve(registryKeysCache)
61
+ }
62
+
41
63
  const registryHost = 'https://registry.npmjs.org'
42
64
  const registryKeysEndpoint = '/-/npm/v1/keys'
43
65
 
@@ -56,10 +78,12 @@ class Marshall extends BaseMarshall {
56
78
  }))
57
79
  })
58
80
  .then((keys) => {
81
+ // save it in the cached singleton object
82
+ registryKeysCache = keys
59
83
  return keys
60
84
  })
61
85
  .catch((error) => {
62
- throw new Error(`Error fetching registry keys: ${error.message}`)
86
+ throw new Warning(`Error fetching registry keys: ${error.message}`)
63
87
  })
64
88
  }
65
89
  }
@@ -8,7 +8,7 @@ const { marshallCategories } = require('./constants')
8
8
 
9
9
  const MARSHALL_NAME = 'snyk'
10
10
  const SNYK_API_URL = 'https://snyk.io/api/v1/vuln/npm'
11
- const SNYK_TEST_URL = 'https://snyk.io/test/npm'
11
+
12
12
  const SNYK_PACKAGE_PAGE = 'https://snyk.io/vuln/npm:'
13
13
  const SNYK_API_TOKEN = process.env.SNYK_TOKEN
14
14
  const SNYK_CONFIG_FILE = '.config/configstore/snyk.json'
@@ -53,34 +53,70 @@ class Marshall extends BaseMarshall {
53
53
  }
54
54
 
55
55
  if (data && data.issuesCount && data.issuesCount > 0) {
56
- const packageSecurityInfo = `${SNYK_PACKAGE_PAGE}${encodeURIComponent(pkg.packageName)}`
57
- throw new Error(`${data.issuesCount} vulnerable path(s) found: ${packageSecurityInfo}`)
56
+ if (this.snykApiToken) {
57
+ const packageSecurityInfo = `${SNYK_PACKAGE_PAGE}${encodeURIComponent(pkg.packageName)}`
58
+ if (data.isMaliciousPackage) {
59
+ throw new Error(`Malicious package found: ${packageSecurityInfo}`)
60
+ }
61
+
62
+ throw new Error(`${data.issuesCount} vulnerable path(s) found: ${packageSecurityInfo}`)
63
+ } else {
64
+ throw new Error(
65
+ `${data.issuesCount} vulnerabilities found by OSV for ${pkg.packageName}`
66
+ )
67
+ }
58
68
  }
59
69
 
60
70
  return data
61
71
  })
62
72
  }
63
73
 
64
- getSnykVulnInfoUnauthenticated({ packageName, packageVersion }) {
65
- const url = encodeURI(`${SNYK_TEST_URL}/${packageName}/${packageVersion}?type=json`)
74
+ getOsvVulnerabilityInfo({ packageName, packageVersion }) {
75
+ const url = 'https://api.osv.dev/v1/query'
76
+ const body = {
77
+ version: packageVersion,
78
+ package: {
79
+ name: packageName,
80
+ ecosystem: 'npm'
81
+ }
82
+ }
66
83
 
67
- return fetch(url)
84
+ return fetch(url, {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json'
88
+ },
89
+ body: JSON.stringify(body)
90
+ })
68
91
  .then((response) => response.json())
69
92
  .then((data) => {
70
- // format returned results the way that the
71
- // official test API endpoint returns them in
72
- if (data && data.hasOwnProperty('totalVulns')) {
93
+ if (data && data.vulns) {
94
+ const isMaliciousPackage = data.vulns.some(
95
+ (vuln) =>
96
+ vuln.database_specific &&
97
+ vuln.database_specific['malicious-packages-origins'] &&
98
+ vuln.database_specific['malicious-packages-origins'].length > 0
99
+ )
100
+
73
101
  return {
74
- issuesCount: data.totalVulns
102
+ issuesCount: data.vulns.length,
103
+ isMaliciousPackage
75
104
  }
76
105
  }
106
+ return {
107
+ issuesCount: 0,
108
+ isMaliciousPackage: false
109
+ }
77
110
  })
78
- .catch(() => false)
111
+ .catch(() => ({
112
+ issuesCount: 0,
113
+ isMaliciousPackage: false
114
+ }))
79
115
  }
80
116
 
81
117
  getSnykVulnInfo({ packageName, packageVersion } = {}) {
82
118
  if (!this.snykApiToken) {
83
- return this.getSnykVulnInfoUnauthenticated({ packageName, packageVersion })
119
+ return this.getOsvVulnerabilityInfo({ packageName, packageVersion })
84
120
  }
85
121
 
86
122
  const url = `${SNYK_API_URL}/${encodeURIComponent(packageName + '@' + packageVersion)}`
@@ -90,17 +126,27 @@ class Marshall extends BaseMarshall {
90
126
  Authorization: `token ${this.snykApiToken}`
91
127
  }
92
128
  })
129
+ .then((response) => {
130
+ if (!response.ok) {
131
+ throw new Error(`Snyk API request failed with status ${response.status}`)
132
+ }
133
+ return response
134
+ })
93
135
  .then((response) => response.json())
94
136
  .then((data) => {
95
137
  if (data && data.vulnerabilities) {
138
+ const isMaliciousPackage = data.vulnerabilities.some(
139
+ (vulnerability) => vulnerability.title === 'Malicious Package'
140
+ )
141
+
96
142
  return {
97
- issuesCount: data.vulnerabilities.length
143
+ issuesCount: data.vulnerabilities.length,
144
+ isMaliciousPackage
98
145
  }
99
146
  }
100
147
 
101
148
  return false
102
149
  })
103
- .catch(() => false)
104
150
  }
105
151
 
106
152
  getSnykToken() {
@@ -112,7 +158,7 @@ class Marshall extends BaseMarshall {
112
158
 
113
159
  try {
114
160
  if (fs.statSync(snykConfigPath)) {
115
- const snykConfig = require(snykConfigPath)
161
+ const snykConfig = JSON.parse(fs.readFileSync(snykConfigPath, 'utf8'))
116
162
  if (snykConfig && snykConfig.api) {
117
163
  return snykConfig.api
118
164
  }
@@ -4,7 +4,7 @@ const BaseMarshall = require('./baseMarshall')
4
4
  const { marshallCategories } = require('./constants')
5
5
 
6
6
  const path = require('path')
7
- const levenshtein = require('fast-levenshtein')
7
+ const { distance } = require('fastest-levenshtein')
8
8
  const topPackagesRawJSON = require(path.join(__dirname, '../../data/top-packages.json'))
9
9
 
10
10
  const MARSHALL_NAME = 'typosquatting'
@@ -25,6 +25,11 @@ class Marshall extends BaseMarshall {
25
25
  let similarPackages = []
26
26
  let packageFoundInTopPackages = false
27
27
  return new Promise((resolve, reject) => {
28
+ // If package is within an allow-list
29
+ if (this.packageRepoUtils.isPackageInAllowList(pkg.packageName)) {
30
+ return resolve([])
31
+ }
32
+
28
33
  for (const popularPackageNameInRepository of topPackagesRawJSON) {
29
34
  // If the package to be installed is itself found within the Top Packages dataset
30
35
  // then we don't report on it
@@ -33,7 +38,7 @@ class Marshall extends BaseMarshall {
33
38
  return resolve([])
34
39
  }
35
40
 
36
- levenshteinDistance = levenshtein.get(pkg.packageName, popularPackageNameInRepository)
41
+ levenshteinDistance = distance(pkg.packageName, popularPackageNameInRepository)
37
42
 
38
43
  if (levenshteinDistance > 0 && levenshteinDistance < 3) {
39
44
  similarPackages.push(popularPackageNameInRepository)
@@ -45,9 +50,7 @@ class Marshall extends BaseMarshall {
45
50
  const uniqueSimilarPackages = [...new Set(similarPackages)]
46
51
  return reject(
47
52
  new Error(
48
- `Package name could be a typosquatting attempt for popular package(s): ${uniqueSimilarPackages.join(
49
- ', '
50
- )}`
53
+ `Potential typosquatting with popular package(s): ${uniqueSimilarPackages.join(', ')}`
51
54
  )
52
55
  )
53
56
  }
@@ -10,7 +10,7 @@ class Marshall extends BaseMarshall {
10
10
  constructor(options) {
11
11
  super(options)
12
12
  this.name = MARSHALL_NAME
13
- this.categoryId = marshallCategories.PackageHealth.id
13
+ this.categoryId = marshallCategories.SupplyChainSecurity.id
14
14
  }
15
15
 
16
16
  title() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npq",
3
- "version": "3.8.1",
3
+ "version": "3.9.0",
4
4
  "description": "marshall your npm/npm package installs with high quality and class 🎖",
5
5
  "bin": {
6
6
  "npq": "./bin/npq.js",
@@ -24,7 +24,7 @@
24
24
  "prepare": "husky install"
25
25
  },
26
26
  "engines": {
27
- "node": ">=18"
27
+ "node": ">=20.13.0"
28
28
  },
29
29
  "author": {
30
30
  "name": "Liran Tal",
@@ -39,6 +39,12 @@
39
39
  "provenance": true,
40
40
  "access": "public"
41
41
  },
42
+ "dependencies": {
43
+ "fastest-levenshtein": "^1.0.16",
44
+ "npm-package-arg": "^11.0.1",
45
+ "pacote": "^17.0.4",
46
+ "semver": "^7.7.2"
47
+ },
42
48
  "devDependencies": {
43
49
  "@babel/core": "^7.23.0",
44
50
  "@babel/plugin-transform-runtime": "^7.22.15",
@@ -66,10 +72,10 @@
66
72
  "collectCoverage": true,
67
73
  "coverageThreshold": {
68
74
  "global": {
69
- "branches": 90,
70
- "functions": 90,
71
- "lines": 90,
72
- "statements": 90
75
+ "branches": 80,
76
+ "functions": 80,
77
+ "lines": 80,
78
+ "statements": 80
73
79
  },
74
80
  "scripts/*": {
75
81
  "branches": 60,
@@ -78,6 +84,9 @@
78
84
  "statements": 80
79
85
  }
80
86
  },
87
+ "testMatch": [
88
+ "**/__tests__/**/*.test.js"
89
+ ],
81
90
  "testPathIgnorePatterns": [
82
91
  "/__tests__/__fixtures__/*"
83
92
  ]
@@ -127,7 +136,7 @@
127
136
  "node/no-unsupported-features": "off",
128
137
  "node/no-unpublished-require": "off",
129
138
  "security/detect-non-literal-fs-filename": "warn",
130
- "security/detect-unsafe-regex": "error",
139
+ "security/detect-unsafe-regex": "warn",
131
140
  "security/detect-buffer-noassert": "error",
132
141
  "security/detect-child-process": "warn",
133
142
  "security/detect-disable-mustache-escape": "error",
@@ -169,18 +178,6 @@
169
178
  }
170
179
  }
171
180
  },
172
- "dependencies": {
173
- "fast-levenshtein": "^3.0.0",
174
- "glob": "^10.3.10",
175
- "inquirer": "^8.2.6",
176
- "kleur": "^4.1.5",
177
- "listr2": "^7.0.1",
178
- "npm-package-arg": "^11.0.1",
179
- "pacote": "^17.0.4",
180
- "semver": "^7.5.4",
181
- "validator": "13.11.0",
182
- "yargs": "^17.7.2"
183
- },
184
181
  "release": {
185
182
  "branches": [
186
183
  "main"
@@ -1,8 +1,8 @@
1
1
  const fs = require('fs')
2
- const color = require('kleur')
3
- const inquirer = require('inquirer')
2
+ const { styleText } = require('node:util')
4
3
  const semver = require('semver')
5
4
 
5
+ const cliPrompt = require('../lib/helpers/cliPrompt.js')
6
6
  const helpers = require('./scriptHelpers')
7
7
 
8
8
  const runPostInstall = async () => {
@@ -35,24 +35,23 @@ const runPostInstall = async () => {
35
35
  console.log(
36
36
  'To do that, we can alias npm and yarn to npq, so that e.g. `npm install <package>` will first use npq to verify the package and prompt you if it finds any issues.'
37
37
  )
38
- const answers = await inquirer.prompt([
39
- {
40
- type: 'confirm',
41
- name: 'install',
42
- message: `Do you want to add ${shellConfig.name} aliases for npm and yarn?`
43
- }
44
- ])
38
+ const answers = await cliPrompt.prompt({
39
+ name: 'install',
40
+ message: `Do you want to add ${shellConfig.name} aliases for npm and yarn?`,
41
+ default: true
42
+ })
43
+
45
44
  if (answers.install) {
46
45
  // eslint-disable-next-line security/detect-non-literal-fs-filename
47
46
  await fs.promises.appendFile(shellConfig.profilePath, shellConfig.aliases)
48
- console.log(color.green('✔'), 'Reload your shell profile to use npq!')
47
+ console.log(styleText('green', '✔'), 'Reload your shell profile to use npq!')
49
48
  }
50
49
  } catch (err) {
51
50
  if (err.isTtyError) {
52
51
  // Could not render inquirer prompt; abort auto-install
53
52
  return
54
53
  }
55
- console.error(color.red('Failed to add aliases: '), err)
54
+ console.error(styleText('red', 'Failed to add aliases: '), err)
56
55
  }
57
56
  }
58
57