npq 3.8.1 → 3.9.1

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 +73 -20
  20. package/bin/npq.js +113 -21
  21. package/lib/cli.js +140 -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,89 @@
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
+
20
+ const silentModeNoPackages = !cliArgs || !cliArgs.packages || cliArgs.packages.length === 0
21
+
22
+ const isInteractive = cliSupport.isInteractiveTerminal() && !silentModeNoPackages
23
+ const spinner = isInteractive ? new Spinner({ text: 'Initiating...' }) : null
24
+
25
+ if (spinner) {
26
+ spinner.start()
27
+ }
21
28
 
22
29
  const marshall = new Marshall({
23
- pkgs: cli.package
30
+ pkgs: cliArgs.packages,
31
+ progressManager: spinner,
32
+ promiseThrottleHelper
24
33
  })
25
34
 
26
35
  marshall
27
36
  .process()
37
+ .then((marshallResults) => {
38
+ if (spinner) {
39
+ spinner.stop()
40
+ }
41
+
42
+ const results = reportResults(marshallResults)
43
+ if (
44
+ results &&
45
+ Object.hasOwn(results, 'countErrors') &&
46
+ Object.hasOwn(results, 'countWarnings')
47
+ ) {
48
+ const { countErrors, countWarnings, useRichFormatting } = results
49
+ const isErrors = countErrors > 0 || countWarnings > 0
50
+
51
+ if (isErrors) {
52
+ console.log()
53
+ console.log('Packages with issues found:')
54
+
55
+ if (useRichFormatting) {
56
+ console.log(results.resultsForPrettyPrint)
57
+ console.log(results.summaryForPrettyPrint)
58
+ } else {
59
+ console.log(results.resultsForPlainTextPrint)
60
+ console.log(results.summaryForPlainTextPrint)
61
+ }
62
+ }
63
+
64
+ return {
65
+ anyIssues: isErrors,
66
+ countErrors,
67
+ countWarnings
68
+ }
69
+ }
70
+ return undefined
71
+ })
28
72
  .then((result) => {
29
- if (result && result.error) {
73
+ if (result && result.countErrors > 0) {
30
74
  // eslint-disable-next-line no-console
31
75
  console.log()
32
- return inquirer.prompt([
33
- {
34
- type: 'confirm',
76
+ return cliPrompt.prompt({
77
+ name: 'install',
78
+ message: 'Continue install ?',
79
+ default: false
80
+ })
81
+ } else {
82
+ if (result && result.countWarnings > 0) {
83
+ // eslint-disable-next-line no-console
84
+ console.log()
85
+ return cliPrompt.autoContinue({
35
86
  name: 'install',
36
- message: 'Would you like to continue installing package(s)?',
37
- default: false
38
- }
39
- ])
87
+ message: 'Auto-continue with install in... ',
88
+ timeInSeconds: 15
89
+ })
90
+ }
40
91
  }
41
92
 
42
93
  return { install: true }
@@ -47,7 +98,9 @@ marshall
47
98
  }
48
99
  })
49
100
  .catch((error) => {
50
- // eslint-disable-next-line no-console
51
- console.error(error)
52
- process.exit(-1)
101
+ CliParser.exit({
102
+ errorCode: error.code || -1,
103
+ message: error.message || 'An error occurred',
104
+ spinner
105
+ })
53
106
  })
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,142 @@
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, earlyExitNoInstall = false) {
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
+ if (earlyExitNoInstall) {
42
+ // If no install command, exit early
43
+ // needed for npq-hero command which only runs on 'install' use-cases of npm
44
+ break
45
+ }
46
+
47
+ // Treat first positional as package if no explicit command
48
+ packages = positionals
49
+ break
50
+ }
51
+ }
52
+
53
+ // Parse and normalize packages
54
+ return packages.map((pkg) => {
55
+ const parsedPackage = npa(pkg)
56
+ const versionModifier = parsedPackage.fetchSpec === '*' ? 'latest' : parsedPackage.fetchSpec
57
+ return `${parsedPackage.name}@${versionModifier}`
58
+ })
59
+ }
60
+
61
+ static parseArgsFull() {
62
+ const options = {
63
+ 'dry-run': { type: 'boolean' },
64
+ plain: { type: 'boolean' },
65
+ packageManager: { type: 'string' },
66
+ pkgMgr: { type: 'string' },
67
+ help: { type: 'boolean', short: 'h' },
68
+ version: { type: 'boolean', short: 'v' }
69
+ }
70
+
71
+ const config = {
72
+ options,
73
+ allowPositionals: true,
74
+ strict: false
75
+ }
76
+
77
+ const { values, positionals } = parseArgs(config)
78
+
79
+ // Handle help
80
+ if (values.help) {
81
+ console.log(`Usage: npq install <package> [options]
82
+
83
+ Commands:
84
+ install [package...] install a package
85
+
86
+ Options:
87
+ --dry-run Run checks only, don't install
88
+ --plain Force non-rich text output
89
+ --packageManager Package Manager to use (default: npm)
90
+ --pkgMgr Alias for packageManager
91
+ -h, --help Show help
92
+ -v, --version Show version
93
+
94
+ Examples:
95
+ npq install express
96
+
97
+ curated by Liran Tal at https://github.com/lirantal/npq`)
98
+ process.exit(0)
99
+ }
100
+
101
+ // Handle version
102
+ if (values.version) {
103
+ console.log(pkg.version)
104
+ process.exit(0)
105
+ }
106
+
107
+ // Process install command and packages
108
+ const normalizedPackages = this._extractPackagesFromPositionals(positionals)
109
+
110
+ return {
111
+ packages: normalizedPackages,
112
+ packageManager: values.packageManager || values.pkgMgr || 'npm',
113
+ dryRun: values['dry-run'] || false,
114
+ plain: values.plain || false
115
+ }
116
+ }
117
+
118
+ static parseArgsMinimal() {
119
+ const config = {
120
+ allowPositionals: true,
121
+ strict: false,
122
+ options: {
123
+ install: {
124
+ type: 'string',
125
+ short: 'i',
126
+ default: 'install'
127
+ }
128
+ }
129
+ }
130
+
131
+ const { positionals } = parseArgs(config)
132
+
133
+ const earlyExitNoInstall = true
134
+ const normalizedPackages = this._extractPackagesFromPositionals(positionals, earlyExitNoInstall)
135
+
136
+ return {
137
+ packages: normalizedPackages
138
+ }
139
+ }
140
+ }
141
+
142
+ 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
+ }