@topcli/prompts 1.0.1 → 1.2.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 +8 -11
- package/index.d.ts +9 -2
- package/package.json +9 -7
- package/src/confirm-prompt.js +71 -25
- package/src/constants.js +23 -9
- package/src/question-prompt.js +23 -10
- package/src/select-prompt.js +8 -6
package/README.md
CHANGED
|
@@ -27,20 +27,15 @@ $ yarn add @topcli/prompts
|
|
|
27
27
|
You can locally run `node ./demo.js`
|
|
28
28
|
|
|
29
29
|
```js
|
|
30
|
-
import {
|
|
30
|
+
import { question, confirm, select } from "@topcli/prompts";
|
|
31
31
|
|
|
32
|
-
const kTestRunner = [
|
|
32
|
+
const kTestRunner = ["node", "tap", "tape", "vitest", "mocha", "ava"];
|
|
33
33
|
|
|
34
|
-
const name = await
|
|
35
|
-
const runner = await select(
|
|
36
|
-
|
|
37
|
-
maxVisible: 5
|
|
38
|
-
});
|
|
39
|
-
const isCLI = await confirm('Your project is a CLI ?', {
|
|
40
|
-
initial: true
|
|
41
|
-
});
|
|
34
|
+
const name = await question("Project name ?", { defaultValue: "foo" });
|
|
35
|
+
const runner = await select("Choose a test runner", { choices: kTestRunner, maxVisible: 5 });
|
|
36
|
+
const isCLI = await confirm("Your project is a CLI ?", { initial: true });
|
|
42
37
|
|
|
43
|
-
console.log(name, runner, isCLI)
|
|
38
|
+
console.log(name, runner, isCLI);
|
|
44
39
|
```
|
|
45
40
|
|
|
46
41
|
## API
|
|
@@ -100,6 +95,7 @@ Boolean prompt, return `options.initial` if user input is different from "y"/"ye
|
|
|
100
95
|
|
|
101
96
|
```ts
|
|
102
97
|
export interface PromptOptions {
|
|
98
|
+
defaultValue?: string;
|
|
103
99
|
validators?: {
|
|
104
100
|
validate: (input: string) => boolean;
|
|
105
101
|
error: (input: string) => string;
|
|
@@ -140,6 +136,7 @@ export interface ConfirmOptions {
|
|
|
140
136
|
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/thomas-gentilhomme/"><img src="https://avatars.githubusercontent.com/u/4438263?v=4?s=100" width="100px;" alt="Gentilhomme"/><br /><sub><b>Gentilhomme</b></sub></a><br /><a href="https://github.com/TopCli/prompts/pulls?q=is%3Apr+reviewed-by%3Afraxken" title="Reviewed Pull Requests">👀</a></td>
|
|
141
137
|
<td align="center" valign="top" width="14.28%"><a href="http://tonygo.dev"><img src="https://avatars0.githubusercontent.com/u/22824417?v=4?s=100" width="100px;" alt="Tony Gorez"/><br /><sub><b>Tony Gorez</b></sub></a><br /><a href="https://github.com/TopCli/prompts/pulls?q=is%3Apr+reviewed-by%3Atony-go" title="Reviewed Pull Requests">👀</a></td>
|
|
142
138
|
<td align="center" valign="top" width="14.28%"><a href="http://sofiand.github.io/portfolio-client/"><img src="https://avatars.githubusercontent.com/u/39944043?v=4?s=100" width="100px;" alt="Yefis"/><br /><sub><b>Yefis</b></sub></a><br /><a href="https://github.com/TopCli/prompts/commits?author=SofianD" title="Code">💻</a> <a href="https://github.com/TopCli/prompts/commits?author=SofianD" title="Documentation">📖</a></td>
|
|
139
|
+
<td align="center" valign="top" width="14.28%"><a href="http://justie.dev"><img src="https://avatars.githubusercontent.com/u/7118300?v=4?s=100" width="100px;" alt="Ben"/><br /><sub><b>Ben</b></sub></a><br /><a href="https://github.com/TopCli/prompts/commits?author=JUSTIVE" title="Documentation">📖</a> <a href="#maintenance-JUSTIVE" title="Maintenance">🚧</a></td>
|
|
143
140
|
</tr>
|
|
144
141
|
</tbody>
|
|
145
142
|
</table>
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface QuestionOptions {
|
|
2
|
+
stdin?: NodeJS.ReadStream & {
|
|
3
|
+
fd: 0;
|
|
4
|
+
};
|
|
5
|
+
stdout?: NodeJS.WriteStream & {
|
|
6
|
+
fd: 1;
|
|
7
|
+
};
|
|
8
|
+
defaultValue?: string;
|
|
2
9
|
validators?: Validator[];
|
|
3
10
|
}
|
|
4
11
|
|
|
@@ -23,7 +30,7 @@ export interface ConfirmOptions {
|
|
|
23
30
|
initial?: boolean;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
export function question(message: string, options?:
|
|
33
|
+
export function question(message: string, options?: QuestionOptions): Promise<string>;
|
|
27
34
|
export function select(message: string, options: SelectOptions): Promise<string>;
|
|
28
35
|
export function confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
29
36
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topcli/prompts",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Node.js user input library for command-line interfaces.",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "node --loader=esmock --test
|
|
6
|
+
"test": "node --no-warnings=ExperimentalWarning --loader=esmock --test test/",
|
|
7
7
|
"coverage": "c8 -r html npm run test",
|
|
8
8
|
"lint": "eslint .",
|
|
9
9
|
"lint:fix": "eslint . --fix"
|
|
@@ -24,13 +24,15 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@nodesecure/eslint-config": "^1.7.0",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
27
|
+
"@types/node": "^20.4.5",
|
|
28
|
+
"c8": "^8.0.0",
|
|
29
|
+
"eslint": "^8.44.0",
|
|
30
|
+
"esmock": "^2.3.1"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"is-unicode-supported": "^1.3.0",
|
|
34
|
+
"kleur": "^4.1.5",
|
|
35
|
+
"strip-ansi": "^7.1.0"
|
|
34
36
|
},
|
|
35
37
|
"engines": {
|
|
36
38
|
"node": ">=14"
|
package/src/confirm-prompt.js
CHANGED
|
@@ -2,13 +2,31 @@
|
|
|
2
2
|
import { EOL } from "node:os";
|
|
3
3
|
|
|
4
4
|
// Import Third-party Dependencies
|
|
5
|
-
import
|
|
5
|
+
import kleur from "kleur";
|
|
6
6
|
|
|
7
7
|
// Import Internal Dependencies
|
|
8
8
|
import { AbstractPrompt } from "./abstract-prompt.js";
|
|
9
9
|
import { SYMBOLS } from "./constants.js";
|
|
10
10
|
|
|
11
|
+
// CONSTANTS
|
|
12
|
+
const kToggleKeys = new Set([
|
|
13
|
+
"left",
|
|
14
|
+
"right",
|
|
15
|
+
"tab",
|
|
16
|
+
"q",
|
|
17
|
+
"a",
|
|
18
|
+
"d",
|
|
19
|
+
"h",
|
|
20
|
+
"j",
|
|
21
|
+
"k",
|
|
22
|
+
"l",
|
|
23
|
+
"space"
|
|
24
|
+
]);
|
|
25
|
+
|
|
11
26
|
export class ConfirmPrompt extends AbstractPrompt {
|
|
27
|
+
#boundKeyPressEvent;
|
|
28
|
+
#boundExitEvent;
|
|
29
|
+
|
|
12
30
|
constructor(message, options = {}) {
|
|
13
31
|
const {
|
|
14
32
|
stdin = process.stdin,
|
|
@@ -17,55 +35,83 @@ export class ConfirmPrompt extends AbstractPrompt {
|
|
|
17
35
|
} = options;
|
|
18
36
|
super(message, stdin, stdout);
|
|
19
37
|
|
|
20
|
-
const Yes = `${ansi.bold.open}Yes${ansi.bold.close}`;
|
|
21
|
-
const No = `${ansi.bold.open}No${ansi.bold.close}`;
|
|
22
|
-
const tip = initial ? `${Yes}/no` : `yes/${No}`;
|
|
23
|
-
|
|
24
38
|
this.initial = initial;
|
|
39
|
+
this.selectedValue = initial;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#getHint() {
|
|
43
|
+
const Yes = kleur.bold().underline().cyan("Yes");
|
|
44
|
+
const No = kleur.bold().underline().cyan("No");
|
|
45
|
+
|
|
46
|
+
return this.selectedValue ? `${Yes}/No` : `Yes/${No}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#render() {
|
|
50
|
+
this.write(this.#getQuestionQuery());
|
|
25
51
|
}
|
|
26
52
|
|
|
27
53
|
#question() {
|
|
28
54
|
return new Promise((resolve) => {
|
|
29
55
|
const questionQuery = this.#getQuestionQuery();
|
|
30
56
|
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
this.write(questionQuery);
|
|
58
|
+
|
|
59
|
+
this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve);
|
|
60
|
+
this.stdin.on("keypress", this.#boundKeyPressEvent);
|
|
34
61
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
62
|
+
this.#boundExitEvent = this.#onProcessExit.bind(this);
|
|
63
|
+
process.once("exit", this.#boundExitEvent);
|
|
38
64
|
});
|
|
39
65
|
}
|
|
40
66
|
|
|
41
|
-
#
|
|
42
|
-
|
|
67
|
+
#onKeypress(resolve, value, key) {
|
|
68
|
+
this.stdin.pause();
|
|
69
|
+
this.stdout.moveCursor(-this.#getQuestionQuery().length, 0);
|
|
70
|
+
this.stdout.clearScreenDown(() => this.stdin.resume());
|
|
71
|
+
|
|
72
|
+
if (key.name === "return") {
|
|
73
|
+
resolve(this.selectedValue);
|
|
74
|
+
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
43
77
|
|
|
44
|
-
|
|
78
|
+
if (kToggleKeys.has(key.name)) {
|
|
79
|
+
this.selectedValue = !this.selectedValue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.#render();
|
|
45
83
|
}
|
|
46
84
|
|
|
47
|
-
#
|
|
48
|
-
this.
|
|
49
|
-
this.write(`${this.answer ? SYMBOLS.Tick : SYMBOLS.Cross} ${this.message}${EOL}`);
|
|
85
|
+
#onProcessExit() {
|
|
86
|
+
this.stdin.off("keypress", this.#boundKeyPressEvent);
|
|
50
87
|
}
|
|
51
88
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
89
|
+
#getQuestionQuery() {
|
|
90
|
+
const query = kleur.bold(`${SYMBOLS.QuestionMark} ${this.message}`);
|
|
56
91
|
|
|
57
|
-
return
|
|
92
|
+
return `${query} ${this.#getHint()}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#onQuestionAnswer() {
|
|
96
|
+
this.clearLastLine();
|
|
97
|
+
this.write(`${this.selectedValue ? SYMBOLS.Tick : SYMBOLS.Cross} ${kleur.bold(this.message)}${EOL}`);
|
|
58
98
|
}
|
|
59
99
|
|
|
60
100
|
async confirm() {
|
|
101
|
+
this.write(SYMBOLS.HideCursor);
|
|
102
|
+
|
|
61
103
|
try {
|
|
62
|
-
|
|
63
|
-
this.answer = this.#validateResult(result) ? ["y", "yes"].includes(result.toLocaleLowerCase()) : this.initial;
|
|
104
|
+
await this.#question();
|
|
64
105
|
this.#onQuestionAnswer();
|
|
65
106
|
|
|
66
|
-
return this.
|
|
107
|
+
return this.selectedValue;
|
|
67
108
|
}
|
|
68
109
|
finally {
|
|
110
|
+
this.write(SYMBOLS.ShowCursor);
|
|
111
|
+
|
|
112
|
+
this.#onProcessExit();
|
|
113
|
+
process.off("exit", this.#boundExitEvent);
|
|
114
|
+
|
|
69
115
|
this.destroy();
|
|
70
116
|
}
|
|
71
117
|
}
|
package/src/constants.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
// Import Third-party Dependencies
|
|
2
|
-
import
|
|
2
|
+
import kleur from "kleur";
|
|
3
|
+
import isUnicodeSupported from "is-unicode-supported";
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
+
const kMainSymbols = {
|
|
6
|
+
tick: "✔",
|
|
7
|
+
cross: "✖",
|
|
8
|
+
pointer: "›",
|
|
9
|
+
previous: "⭡",
|
|
10
|
+
next: "⭣"
|
|
11
|
+
};
|
|
12
|
+
const kFallbackSymbols = {
|
|
13
|
+
tick: "√",
|
|
14
|
+
cross: "×",
|
|
15
|
+
pointer: ">",
|
|
16
|
+
previous: "↑",
|
|
17
|
+
next: "↓"
|
|
18
|
+
};
|
|
19
|
+
const kSymbols = isUnicodeSupported() ? kMainSymbols : kFallbackSymbols;
|
|
20
|
+
const kPointer = kleur.gray(kSymbols.pointer);
|
|
5
21
|
|
|
6
22
|
export const SYMBOLS = {
|
|
7
|
-
QuestionMark:
|
|
8
|
-
Tick:
|
|
9
|
-
Cross:
|
|
23
|
+
QuestionMark: kleur.blue().bold("?"),
|
|
24
|
+
Tick: kleur.green().bold(kSymbols.tick),
|
|
25
|
+
Cross: kleur.red().bold(kSymbols.cross),
|
|
10
26
|
Pointer: kPointer,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Previous: "⭡",
|
|
14
|
-
Next: "⭣",
|
|
27
|
+
Previous: kSymbols.previous,
|
|
28
|
+
Next: kSymbols.next,
|
|
15
29
|
ShowCursor: "\x1B[?25h",
|
|
16
30
|
HideCursor: "\x1B[?25l"
|
|
17
31
|
};
|
package/src/question-prompt.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { EOL } from "node:os";
|
|
3
3
|
|
|
4
4
|
// Import Third-party Dependencies
|
|
5
|
-
import
|
|
6
|
-
import stripAnsi from "strip-ansi";
|
|
5
|
+
import kleur from "kleur";
|
|
7
6
|
|
|
8
7
|
// Import Internal Dependencies
|
|
9
8
|
import { AbstractPrompt } from "./abstract-prompt.js";
|
|
@@ -13,12 +12,22 @@ export class QuestionPrompt extends AbstractPrompt {
|
|
|
13
12
|
#validators;
|
|
14
13
|
|
|
15
14
|
constructor(message, options = {}) {
|
|
16
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
stdin = process.stdin,
|
|
17
|
+
stdout = process.stdout,
|
|
18
|
+
defaultValue,
|
|
19
|
+
validators = []
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
17
22
|
super(message, stdin, stdout);
|
|
18
23
|
|
|
24
|
+
if (defaultValue && typeof defaultValue !== "string") {
|
|
25
|
+
throw new TypeError("defaultValue must be a string");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.defaultValue = defaultValue;
|
|
29
|
+
this.tip = this.defaultValue ? ` (${this.defaultValue})` : "";
|
|
19
30
|
this.#validators = validators;
|
|
20
|
-
this.questionPrefix = `${ansi.bold.open}${SYMBOLS.QuestionMark} `;
|
|
21
|
-
this.questionSuffix = `${ansi.bold.close} `;
|
|
22
31
|
this.questionSuffixError = "";
|
|
23
32
|
}
|
|
24
33
|
|
|
@@ -35,19 +44,19 @@ export class QuestionPrompt extends AbstractPrompt {
|
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
#getQuestionQuery() {
|
|
38
|
-
return `${
|
|
47
|
+
return `${kleur.bold(`${SYMBOLS.QuestionMark} ${this.message}${this.tip}`)} ${this.questionSuffixError}`;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
#setQuestionSuffixError(error) {
|
|
42
|
-
const suffix =
|
|
51
|
+
const suffix = kleur.red(`[${error}] `);
|
|
43
52
|
this.questionSuffixError = suffix;
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
#writeAnswer() {
|
|
47
|
-
const prefix =
|
|
48
|
-
const
|
|
56
|
+
const prefix = this.answer ? SYMBOLS.Tick : SYMBOLS.Cross;
|
|
57
|
+
const answer = kleur.yellow(this.answer ?? "");
|
|
49
58
|
|
|
50
|
-
this.write(`${prefix} ${this.message} ${SYMBOLS.Pointer} ${
|
|
59
|
+
this.write(`${prefix} ${kleur.bold(this.message)} ${SYMBOLS.Pointer} ${answer}${EOL}`);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
#onQuestionAnswer() {
|
|
@@ -69,6 +78,10 @@ export class QuestionPrompt extends AbstractPrompt {
|
|
|
69
78
|
async question() {
|
|
70
79
|
this.answer = await this.#question();
|
|
71
80
|
|
|
81
|
+
if (this.answer === "" && this.defaultValue) {
|
|
82
|
+
this.answer = this.defaultValue;
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
this.#onQuestionAnswer();
|
|
73
86
|
|
|
74
87
|
while (this.answer?.constructor.name === "Promise") {
|
package/src/select-prompt.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { EOL } from "node:os";
|
|
3
3
|
|
|
4
4
|
// Import Third-party Dependencies
|
|
5
|
-
import
|
|
5
|
+
import kleur from "kleur";
|
|
6
6
|
|
|
7
7
|
// Import Internal Dependencies
|
|
8
8
|
import { AbstractPrompt } from "./abstract-prompt.js";
|
|
@@ -82,6 +82,7 @@ export class SelectPrompt extends AbstractPrompt {
|
|
|
82
82
|
|
|
83
83
|
for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) {
|
|
84
84
|
const choice = this.#getFormattedChoice(choiceIndex);
|
|
85
|
+
const isChoiceSelected = choiceIndex === this.activeIndex;
|
|
85
86
|
const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex;
|
|
86
87
|
const showNextChoicesArrow = endIndex < this.choices.length && choiceIndex === endIndex - 1;
|
|
87
88
|
|
|
@@ -93,20 +94,21 @@ export class SelectPrompt extends AbstractPrompt {
|
|
|
93
94
|
prefixArrow = SYMBOLS.Next;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
const prefix = `${prefixArrow}${
|
|
97
|
+
const prefix = `${prefixArrow}${isChoiceSelected ? `${SYMBOLS.Pointer} ` : " "}`;
|
|
97
98
|
const formattedLabel = choice.label.padEnd(
|
|
98
99
|
this.longestChoice < 10 ? this.longestChoice : 0
|
|
99
100
|
);
|
|
100
101
|
const formattedDescription = choice.description ? ` - ${choice.description}` : "";
|
|
101
|
-
const
|
|
102
|
+
const color = isChoiceSelected ? kleur.white().bold : kleur.gray;
|
|
103
|
+
const str = color(`${prefix}${formattedLabel}${formattedDescription}${EOL}`);
|
|
102
104
|
|
|
103
105
|
this.write(str);
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
#showAnsweredQuestion(choice) {
|
|
108
|
-
const prefix = `${
|
|
109
|
-
const formattedChoice =
|
|
110
|
+
const prefix = `${SYMBOLS.Tick} ${kleur.bold(this.message)} ${SYMBOLS.Pointer}`;
|
|
111
|
+
const formattedChoice = kleur.yellow(choice.label ?? choice);
|
|
110
112
|
|
|
111
113
|
this.write(`${prefix} ${formattedChoice}${EOL}`);
|
|
112
114
|
}
|
|
@@ -175,6 +177,6 @@ export class SelectPrompt extends AbstractPrompt {
|
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
#showQuestion() {
|
|
178
|
-
this.write(`${
|
|
180
|
+
this.write(`${SYMBOLS.QuestionMark} ${kleur.bold(this.message)}${EOL}`);
|
|
179
181
|
}
|
|
180
182
|
}
|