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.
- package/.github/npq.mov +0 -0
- package/.github/npq.mp4 +0 -0
- package/.github/npq.png +0 -0
- package/.github/workflows/main.yml +2 -2
- package/README.md +18 -9
- package/__tests__/__fixtures__/test.marshall.js +5 -6
- package/__tests__/cli.test.js +110 -0
- package/__tests__/cliPrompt.test.js +409 -0
- package/__tests__/marshalls.base.test.js +1 -1
- package/__tests__/marshalls.classMethods.test.js +11 -14
- package/__tests__/marshalls.expiredDomains.test.js +2 -2
- package/__tests__/marshalls.provenance.test.js +181 -4
- package/__tests__/marshalls.signatures.test.js +0 -4
- package/__tests__/marshalls.snyk.test.js +294 -0
- package/__tests__/marshalls.tasks.test.js +48 -24
- package/__tests__/marshalls.typosquatting.test.js +16 -9
- package/__tests__/reportResults.test.js +772 -0
- package/__tests__/scripts.test.js +2 -2
- package/bin/npq-hero.js +73 -20
- package/bin/npq.js +113 -21
- package/lib/cli.js +140 -24
- package/lib/helpers/cliPrompt.js +97 -0
- package/lib/helpers/cliSpinner.js +104 -0
- package/lib/helpers/cliSupportHandler.js +50 -7
- package/lib/helpers/packageRepoUtils.js +5 -0
- package/lib/helpers/promiseThrottler.js +96 -0
- package/lib/helpers/reportResults.js +304 -0
- package/lib/helpers/sourcePackages.js +36 -0
- package/lib/marshall.js +42 -65
- package/lib/marshalls/age.marshall.js +3 -2
- package/lib/marshalls/author.marshall.js +36 -7
- package/lib/marshalls/baseMarshall.js +9 -17
- package/lib/marshalls/deprecation.marshall.js +43 -0
- package/lib/marshalls/downloads.marshall.js +1 -1
- package/lib/marshalls/expiredDomains.marshall.js +7 -5
- package/lib/marshalls/index.js +110 -80
- package/lib/marshalls/license.marshall.js +1 -1
- package/lib/marshalls/newbin.marshall.js +1 -2
- package/lib/marshalls/provenance.marshall.js +152 -33
- package/lib/marshalls/repo.marshall.js +6 -5
- package/lib/marshalls/scripts.marshall.js +1 -1
- package/lib/marshalls/signatures.marshall.js +26 -2
- package/lib/marshalls/snyk.marshall.js +61 -15
- package/lib/marshalls/typosquatting.marshall.js +8 -5
- package/lib/marshalls/version-maturity.marshall.js +1 -1
- package/package.json +16 -19
- package/scripts/postinstall.js +10 -11
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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:
|
|
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.
|
|
73
|
+
if (result && result.countErrors > 0) {
|
|
30
74
|
// eslint-disable-next-line no-console
|
|
31
75
|
console.log()
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
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: '
|
|
37
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
9
|
-
const
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
83
|
+
return {
|
|
84
|
+
anyIssues: isErrors,
|
|
85
|
+
countErrors,
|
|
86
|
+
countWarnings
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined
|
|
90
|
+
})
|
|
19
91
|
.then((result) => {
|
|
20
|
-
if (
|
|
21
|
-
|
|
92
|
+
if (cliArgs.dryRun) {
|
|
93
|
+
CliParser.exit({
|
|
94
|
+
errorCode: 0,
|
|
95
|
+
spinner
|
|
96
|
+
})
|
|
22
97
|
}
|
|
23
98
|
|
|
24
|
-
if (result && result.
|
|
99
|
+
if (result && result.countErrors > 0) {
|
|
25
100
|
// eslint-disable-next-line no-console
|
|
26
101
|
console.log()
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
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: '
|
|
32
|
-
|
|
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(
|
|
123
|
+
pkgMgr.process(cliArgs.packageManager)
|
|
42
124
|
}
|
|
43
125
|
})
|
|
44
126
|
.catch((error) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
}
|