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,7 +1,7 @@
1
1
  const fs = require('fs')
2
2
  const os = require('os')
3
3
  const path = require('path')
4
- const inquirer = require('inquirer')
4
+ const cliPrompt = require('../lib/helpers/cliPrompt.js')
5
5
 
6
6
  const postinstall = require('../scripts/postinstall').testable
7
7
  const preuninstall = require('../scripts/preuninstall').testable
@@ -9,7 +9,7 @@ const helpers = require('../scripts/scriptHelpers')
9
9
 
10
10
  const TEST_PROFILE_PATH = path.resolve(os.tmpdir(), '.npq_test_profile')
11
11
  const TEST_ALIAS = 'alias npm="npq-hero"'
12
- inquirer.prompt = jest.fn().mockResolvedValue({ install: true })
12
+ cliPrompt.prompt = jest.fn().mockResolvedValue({ install: true })
13
13
 
14
14
  let mockGetShellConfig
15
15
  let mockIsRunningInYarn
package/bin/npq-hero.js CHANGED
@@ -5,38 +5,86 @@
5
5
  const cliSupport = require('../lib/helpers/cliSupportHandler')
6
6
  cliSupport.isEnvSupport() || (cliSupport.noSupportError() && cliSupport.packageManagerPassthrough())
7
7
 
8
- const inquirer = require('inquirer')
9
- const yargs = require('yargs')
10
8
  const pkgMgr = require('../lib/packageManager')
11
9
  const Marshall = require('../lib/marshall')
12
- const cliCommons = require('../lib/cliCommons')
10
+ const { CliParser } = require('../lib/cli')
11
+ const cliPrompt = require('../lib/helpers/cliPrompt.js')
12
+ const { reportResults } = require('../lib/helpers/reportResults')
13
+ const { Spinner } = require('../lib/helpers/cliSpinner')
14
+ const { promiseThrottleHelper } = require('../lib/helpers/promiseThrottler')
13
15
 
14
16
  const PACKAGE_MANAGER_TOOL = process.env.NPQ_PKG_MGR
15
17
 
16
- const cli = yargs
17
- .options(cliCommons.getOptions())
18
- .command(cliCommons.getInstallCommand())
19
- .help(false)
20
- .version(false).argv
18
+ const cliArgs = CliParser.parseArgsMinimal()
19
+ const isInteractive = cliSupport.isInteractiveTerminal()
20
+ const spinner = isInteractive ? new Spinner({ text: 'Initiating...' }) : null
21
+
22
+ if (spinner) {
23
+ spinner.start()
24
+ }
21
25
 
22
26
  const marshall = new Marshall({
23
- pkgs: cli.package
27
+ pkgs: cliArgs.packages,
28
+ progressManager: spinner,
29
+ promiseThrottleHelper
24
30
  })
25
31
 
26
32
  marshall
27
33
  .process()
34
+ .then((marshallResults) => {
35
+ if (spinner) {
36
+ spinner.stop()
37
+ }
38
+
39
+ const results = reportResults(marshallResults)
40
+ if (
41
+ results &&
42
+ Object.hasOwn(results, 'countErrors') &&
43
+ Object.hasOwn(results, 'countWarnings')
44
+ ) {
45
+ const { countErrors, countWarnings, useRichFormatting } = results
46
+ const isErrors = countErrors > 0 || countWarnings > 0
47
+
48
+ if (isErrors) {
49
+ console.log()
50
+ console.log('Packages with issues found:')
51
+
52
+ if (useRichFormatting) {
53
+ console.log(results.resultsForPrettyPrint)
54
+ console.log(results.summaryForPrettyPrint)
55
+ } else {
56
+ console.log(results.resultsForPlainTextPrint)
57
+ console.log(results.summaryForPlainTextPrint)
58
+ }
59
+ }
60
+
61
+ return {
62
+ anyIssues: isErrors,
63
+ countErrors,
64
+ countWarnings
65
+ }
66
+ }
67
+ return undefined
68
+ })
28
69
  .then((result) => {
29
- if (result && result.error) {
70
+ if (result && result.countErrors > 0) {
30
71
  // eslint-disable-next-line no-console
31
72
  console.log()
32
- return inquirer.prompt([
33
- {
34
- type: 'confirm',
73
+ return cliPrompt.prompt({
74
+ name: 'install',
75
+ message: 'Continue install ?',
76
+ default: false
77
+ })
78
+ } else {
79
+ if (result && result.countWarnings > 0) {
80
+ // eslint-disable-next-line no-console
81
+ console.log()
82
+ return cliPrompt.autoContinue({
35
83
  name: 'install',
36
- message: 'Would you like to continue installing package(s)?',
37
- default: false
38
- }
39
- ])
84
+ message: 'Auto-continue with install in... ',
85
+ timeInSeconds: 15
86
+ })
87
+ }
40
88
  }
41
89
 
42
90
  return { install: true }
@@ -47,7 +95,9 @@ marshall
47
95
  }
48
96
  })
49
97
  .catch((error) => {
50
- // eslint-disable-next-line no-console
51
- console.error(error)
52
- process.exit(-1)
98
+ CliParser.exit({
99
+ errorCode: error.code || -1,
100
+ message: error.message || 'An error occurred',
101
+ spinner
102
+ })
53
103
  })
package/bin/npq.js CHANGED
@@ -1,48 +1,140 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
+ const util = require('node:util')
5
+
4
6
  // Require minimum node version or bail out
5
7
  const cliSupport = require('../lib/helpers/cliSupportHandler')
6
8
  cliSupport.isEnvSupport() || cliSupport.noSupportError(true)
7
9
 
8
- const inquirer = require('inquirer')
9
- const cli = require('../lib/cli')
10
+ const { getProjectPackages } = require('../lib/helpers/sourcePackages')
11
+ const { CliParser } = require('../lib/cli')
10
12
  const pkgMgr = require('../lib/packageManager')
11
13
  const Marshall = require('../lib/marshall')
14
+ const cliPrompt = require('../lib/helpers/cliPrompt.js')
15
+ const { reportResults } = require('../lib/helpers/reportResults')
16
+ const { Spinner } = require('../lib/helpers/cliSpinner')
17
+ const { promiseThrottleHelper } = require('../lib/helpers/promiseThrottler')
12
18
 
13
- const marshall = new Marshall({
14
- pkgs: cli.package
15
- })
19
+ const debug = util.debuglog('npq')
20
+
21
+ const cliArgs = CliParser.parseArgsFull()
22
+ const isInteractive = cliSupport.isInteractiveTerminal() && !cliArgs.plain
23
+ const spinner = isInteractive ? new Spinner({ text: 'Initiating...' }) : null
24
+
25
+ if (spinner) {
26
+ spinner.start()
27
+ }
28
+
29
+ Promise.resolve()
30
+ .then(() => {
31
+ if (cliArgs.packages.length === 0) {
32
+ debug('\nNo packages specified, using project packages from package.json')
33
+ return getProjectPackages()
34
+ }
35
+
36
+ return cliArgs.packages
37
+ })
38
+ .then((packages) => {
39
+ if (packages.error) {
40
+ console.log()
41
+ CliParser.exit({
42
+ errorCode: packages.error.code || -1,
43
+ message: packages.message,
44
+ spinner
45
+ })
46
+ }
47
+
48
+ const marshall = new Marshall({
49
+ pkgs: packages,
50
+ progressManager: spinner,
51
+ promiseThrottleHelper
52
+ })
53
+
54
+ return marshall.process()
55
+ })
56
+ .then((marshallResults) => {
57
+ if (spinner) {
58
+ spinner.stop()
59
+ }
60
+
61
+ const results = reportResults(marshallResults, { plain: cliArgs.plain })
62
+ if (
63
+ results &&
64
+ Object.hasOwn(results, 'countErrors') &&
65
+ Object.hasOwn(results, 'countWarnings')
66
+ ) {
67
+ const { countErrors, countWarnings, useRichFormatting } = results
68
+ const isErrors = countErrors > 0 || countWarnings > 0
69
+
70
+ if (isErrors) {
71
+ console.log()
72
+ console.log('Packages with issues found:')
73
+
74
+ if (useRichFormatting) {
75
+ console.log(results.resultsForPrettyPrint)
76
+ console.log(results.summaryForPrettyPrint)
77
+ } else {
78
+ console.log(results.resultsForPlainTextPrint)
79
+ console.log(results.summaryForPlainTextPrint)
80
+ }
81
+ }
16
82
 
17
- marshall
18
- .process()
83
+ return {
84
+ anyIssues: isErrors,
85
+ countErrors,
86
+ countWarnings
87
+ }
88
+ }
89
+ return undefined
90
+ })
19
91
  .then((result) => {
20
- if (cli.dryRun) {
21
- process.exit(0)
92
+ if (cliArgs.dryRun) {
93
+ CliParser.exit({
94
+ errorCode: 0,
95
+ spinner
96
+ })
22
97
  }
23
98
 
24
- if (result && result.error) {
99
+ if (result && result.countErrors > 0) {
25
100
  // eslint-disable-next-line no-console
26
101
  console.log()
27
- return inquirer.prompt([
28
- {
29
- type: 'confirm',
102
+ return cliPrompt.prompt({
103
+ name: 'install',
104
+ message: 'Continue install ?',
105
+ default: false
106
+ })
107
+ } else {
108
+ if (result && result.countWarnings > 0) {
109
+ // eslint-disable-next-line no-console
110
+ console.log()
111
+ return cliPrompt.autoContinue({
30
112
  name: 'install',
31
- message: 'Would you like to continue installing package(s)?',
32
- default: false
33
- }
34
- ])
113
+ message: 'Auto-continue with install in... ',
114
+ timeInSeconds: 15
115
+ })
116
+ }
35
117
  }
36
118
 
37
119
  return { install: true }
38
120
  })
39
121
  .then((status) => {
40
122
  if (status && status.hasOwnProperty('install') && status.install === true) {
41
- pkgMgr.process(cli.packageManager)
123
+ pkgMgr.process(cliArgs.packageManager)
42
124
  }
43
125
  })
44
126
  .catch((error) => {
45
- // eslint-disable-next-line no-console
46
- console.error(error)
47
- process.exit(-1)
127
+ CliParser.exit({
128
+ errorCode: error.code || -1,
129
+ message: error.message || 'An error occurred',
130
+ spinner
131
+ })
48
132
  })
133
+
134
+ // attach event handler for CTRL+C
135
+ process.on('SIGINT', () => {
136
+ CliParser.exit({
137
+ errorCode: 0,
138
+ spinner
139
+ })
140
+ })
package/lib/cli.js CHANGED
@@ -1,26 +1,129 @@
1
1
  'use strict'
2
2
 
3
- const yargs = require('yargs')
4
- const cliCommons = require('./cliCommons')
5
-
6
- const argv = yargs
7
- .version()
8
- .usage('Usage: npq install <package> [options]')
9
- .help('help')
10
- .alias('help', 'h')
11
- .options(cliCommons.getOptions())
12
- .command(cliCommons.getInstallCommand())
13
- .command({
14
- command: '--packageManager [packageManager]',
15
- aliases: ['--pkgMgr'],
16
- desc: 'the package manager to offload handling the command',
17
- builder: (yargs) => yargs.default('packageManager', 'npm')
18
- })
19
- .command({
20
- command: '--dry-run',
21
- desc: 'npq will run checks only and will not proceed with actually installing a package'
22
- })
23
- .example('npq install express')
24
- .epilogue('curated by Liran Tal at https://github.com/lirantal/npq').argv
25
-
26
- module.exports = argv
3
+ const { parseArgs } = require('node:util')
4
+ const npa = require('npm-package-arg')
5
+ const pkg = require('../package.json')
6
+
7
+ class CliParser {
8
+ static exit({ errorCode, message, spinner }) {
9
+ if (spinner && spinner.isSpinning) {
10
+ spinner.stop()
11
+ }
12
+
13
+ if (message) {
14
+ console.error(message)
15
+ }
16
+
17
+ process.exit(errorCode || -1)
18
+ }
19
+
20
+ static _extractPackagesFromPositionals(positionals) {
21
+ let packages = []
22
+ if (positionals.length > 0) {
23
+ const command = positionals[0]
24
+
25
+ switch (command) {
26
+ case 'install':
27
+ case 'i':
28
+ case 'add':
29
+ case 'isntall':
30
+ case 'in':
31
+ case 'ins':
32
+ case 'inst':
33
+ case 'insta':
34
+ case 'instal':
35
+ case 'isnt':
36
+ case 'isnta':
37
+ case 'isntal':
38
+ packages = positionals.slice(1)
39
+ break
40
+ default:
41
+ // Treat first positional as package if no explicit command
42
+ packages = positionals
43
+ break
44
+ }
45
+ }
46
+
47
+ // Parse and normalize packages
48
+ return packages.map((pkg) => {
49
+ const parsedPackage = npa(pkg)
50
+ const versionModifier = parsedPackage.fetchSpec === '*' ? 'latest' : parsedPackage.fetchSpec
51
+ return `${parsedPackage.name}@${versionModifier}`
52
+ })
53
+ }
54
+
55
+ static parseArgsFull() {
56
+ const options = {
57
+ 'dry-run': { type: 'boolean' },
58
+ plain: { type: 'boolean' },
59
+ packageManager: { type: 'string' },
60
+ pkgMgr: { type: 'string' },
61
+ help: { type: 'boolean', short: 'h' },
62
+ version: { type: 'boolean', short: 'v' }
63
+ }
64
+
65
+ const config = {
66
+ options,
67
+ allowPositionals: true,
68
+ strict: false
69
+ }
70
+
71
+ const { values, positionals } = parseArgs(config)
72
+
73
+ // Handle help
74
+ if (values.help) {
75
+ console.log(`Usage: npq install <package> [options]
76
+
77
+ Commands:
78
+ install [package...] install a package
79
+
80
+ Options:
81
+ --dry-run Run checks only, don't install
82
+ --plain Force non-rich text output
83
+ --packageManager Package Manager to use (default: npm)
84
+ --pkgMgr Alias for packageManager
85
+ -h, --help Show help
86
+ -v, --version Show version
87
+
88
+ Examples:
89
+ npq install express
90
+
91
+ curated by Liran Tal at https://github.com/lirantal/npq`)
92
+ process.exit(0)
93
+ }
94
+
95
+ // Handle version
96
+ if (values.version) {
97
+ console.log(pkg.version)
98
+ process.exit(0)
99
+ }
100
+
101
+ // Process install command and packages
102
+ const normalizedPackages = this._extractPackagesFromPositionals(positionals)
103
+
104
+ return {
105
+ packages: normalizedPackages,
106
+ packageManager: values.packageManager || values.pkgMgr || 'npm',
107
+ dryRun: values['dry-run'] || false,
108
+ plain: values.plain || false
109
+ }
110
+ }
111
+
112
+ static parseArgsMinimal() {
113
+ const config = {
114
+ allowPositionals: true,
115
+ strict: false,
116
+ options: {}
117
+ }
118
+
119
+ const { positionals } = parseArgs(config)
120
+
121
+ const normalizedPackages = this._extractPackagesFromPositionals(positionals)
122
+
123
+ return {
124
+ packages: normalizedPackages
125
+ }
126
+ }
127
+ }
128
+
129
+ module.exports.CliParser = CliParser
@@ -0,0 +1,97 @@
1
+ 'use strict'
2
+
3
+ const { setTimeout } = require('node:timers/promises')
4
+ const readline = require('node:readline/promises')
5
+ const { stdin, stdout } = require('node:process')
6
+
7
+ /**
8
+ * Simple CLI prompt for yes/no confirmation
9
+ * @param {Object} options - Prompt options
10
+ * @param {string} options.name - The property name for the result (e.g., 'install')
11
+ * @param {string} options.message - The question to ask
12
+ * @param {boolean} [options.default=false] - Default answer if user just presses Enter
13
+ * @returns {Promise<Object>} - Object with the specified property name and boolean value
14
+ */
15
+ async function prompt(options = {}) {
16
+ const { name, message, default: defaultValue = false } = options
17
+
18
+ if (!message) {
19
+ throw new Error('Message is required for prompt')
20
+ }
21
+
22
+ if (!name) {
23
+ throw new Error('Name is required for prompt')
24
+ }
25
+
26
+ const rl = readline.createInterface({
27
+ input: stdin,
28
+ output: stdout
29
+ })
30
+
31
+ try {
32
+ // Format the prompt message with default indicator
33
+ const defaultIndicator = defaultValue ? 'Y/n' : 'y/N'
34
+ const promptMessage = `${message} (${defaultIndicator}) `
35
+
36
+ const answer = await rl.question(promptMessage)
37
+
38
+ // Parse the answer
39
+ const normalizedAnswer = answer.trim().toLowerCase()
40
+
41
+ let result
42
+ if (normalizedAnswer === '') {
43
+ // User pressed Enter without typing anything, use default
44
+ result = defaultValue
45
+ } else if (['y', 'yes'].includes(normalizedAnswer)) {
46
+ result = true
47
+ } else if (['n', 'no'].includes(normalizedAnswer)) {
48
+ result = false
49
+ } else {
50
+ // Invalid input, ask again
51
+ rl.close()
52
+ console.log('Please answer with y/yes or n/no.')
53
+ return prompt(options)
54
+ }
55
+
56
+ return { [name]: result }
57
+ } finally {
58
+ rl.close()
59
+ }
60
+ }
61
+
62
+ async function autoContinue({ name, message, timeInSeconds = 5 } = {}) {
63
+ // Show initial message with countdown
64
+ process.stdout.write(`${message}${timeInSeconds}`)
65
+
66
+ // Count down from timeInSeconds-1 to 1
67
+ for (let i = timeInSeconds - 1; i > 0; i--) {
68
+ await setTimeout(1000)
69
+
70
+ // Calculate how many characters to backspace (previous number length)
71
+ const prevNumber = i + 1
72
+ const prevLength = prevNumber.toString().length
73
+ const currentLength = i.toString().length
74
+
75
+ // Backspace to beginning of number
76
+ const backspaces = '\b'.repeat(prevLength)
77
+
78
+ // Write new number and pad with spaces if needed to clear any remaining digits
79
+ const padding = ' '.repeat(Math.max(0, prevLength - currentLength))
80
+ const moveBack = '\b'.repeat(Math.max(0, prevLength - currentLength))
81
+
82
+ process.stdout.write(`${backspaces}${i}${padding}${moveBack}`)
83
+ }
84
+
85
+ // Wait for the final second
86
+ await setTimeout(1000)
87
+
88
+ // Move to next line after countdown completes
89
+ console.log()
90
+
91
+ return { [name]: true }
92
+ }
93
+
94
+ module.exports = {
95
+ prompt,
96
+ autoContinue
97
+ }
@@ -0,0 +1,104 @@
1
+ const growVertical = {
2
+ interval: 120,
3
+ frames: ['▁', '▃', '▄', '▅', '▆', '▇', '▆', '▅', '▄', '▃']
4
+ }
5
+
6
+ class Spinner {
7
+ constructor(options = {}) {
8
+ this.spinner = options.spinner || growVertical
9
+ this.text = options.text || ''
10
+ this.color = options.color || null
11
+ this.stream = options.stream || process.stderr
12
+ this.frameIndex = 0
13
+ this.timer = null
14
+ this.isSpinning = false
15
+ }
16
+
17
+ start(text) {
18
+ if (this.isSpinning) {
19
+ return
20
+ }
21
+
22
+ if (text) {
23
+ this.text = text
24
+ }
25
+
26
+ this.isSpinning = true
27
+ this.frameIndex = 0
28
+
29
+ // Hide cursor
30
+ this.stream.write('\u001B[?25l')
31
+
32
+ this.timer = setInterval(() => {
33
+ this.render()
34
+ }, this.spinner.interval)
35
+
36
+ this.render()
37
+ }
38
+
39
+ stop(finalText) {
40
+ if (!this.isSpinning) {
41
+ return
42
+ }
43
+
44
+ clearInterval(this.timer)
45
+ this.isSpinning = false
46
+
47
+ // Clear current line and show cursor
48
+ this.stream.write('\r\u001B[K\u001B[?25h')
49
+
50
+ if (finalText) {
51
+ this.stream.write(finalText + '\n')
52
+ }
53
+ }
54
+
55
+ succeed(text) {
56
+ this.stop()
57
+ const message = text || this.text
58
+ this.stream.write(`✅ ${message}\n`)
59
+ }
60
+
61
+ fail(text) {
62
+ this.stop()
63
+ const message = text || this.text
64
+ this.stream.write(`❌ ${message}\n`)
65
+ }
66
+
67
+ warn(text) {
68
+ this.stop()
69
+ const message = text || this.text
70
+ this.stream.write(`⚠️ ${message}\n`)
71
+ }
72
+
73
+ info(text) {
74
+ this.stop()
75
+ const message = text || this.text
76
+ this.stream.write(`ℹ️ ${message}\n`)
77
+ }
78
+
79
+ setText(text) {
80
+ this.text = text
81
+ }
82
+
83
+ update(text) {
84
+ this.text = text
85
+ if (this.isSpinning) {
86
+ this.render()
87
+ }
88
+ }
89
+
90
+ render() {
91
+ const frame = this.spinner.frames[this.frameIndex]
92
+ const output = `${frame} ${this.text}`
93
+
94
+ // Move cursor to beginning of line and clear it
95
+ this.stream.write('\r\u001B[K')
96
+ this.stream.write(output)
97
+
98
+ this.frameIndex = (this.frameIndex + 1) % this.spinner.frames.length
99
+ }
100
+ }
101
+
102
+ module.exports = {
103
+ Spinner
104
+ }