@topcli/prompts 1.4.0 → 1.5.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/README.md CHANGED
@@ -51,6 +51,7 @@ question(message: string, options?: PromptOptions): Promise<string>
51
51
  ```
52
52
 
53
53
  Simple prompt, similar to `rl.question()` with an improved UI.
54
+ Use `options.secure` if you need to hide both input and answer.
54
55
  Use `options.validators` to handle user input.
55
56
 
56
57
  **Example**
@@ -156,6 +157,7 @@ export interface Validator {
156
157
  export interface QuestionOptions extends SharedOptions {
157
158
  defaultValue?: string;
158
159
  validators?: Validator[];
160
+ secure?: boolean;
159
161
  }
160
162
 
161
163
  export interface Choice {
package/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export interface Validator {
15
15
  export interface QuestionOptions extends SharedOptions {
16
16
  defaultValue?: string;
17
17
  validators?: Validator[];
18
+ secure?: boolean;
18
19
  }
19
20
 
20
21
  export interface Choice {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topcli/prompts",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Node.js user input library for command-line interfaces.",
5
5
  "scripts": {
6
6
  "test": "node --no-warnings=ExperimentalWarning --loader=esmock --test test/",
@@ -24,13 +24,15 @@
24
24
  "type": "module",
25
25
  "devDependencies": {
26
26
  "@nodesecure/eslint-config": "^1.8.0",
27
- "@types/node": "^20.8.2",
27
+ "@types/node": "^20.8.5",
28
28
  "c8": "^8.0.1",
29
- "eslint": "^8.50.0",
30
- "esmock": "^2.5.1"
29
+ "eslint": "^8.51.0",
30
+ "esmock": "^2.5.2"
31
31
  },
32
32
  "dependencies": {
33
- "is-unicode-supported": "^1.3.0",
33
+ "@topcli/wcwidth": "^1.0.1",
34
+ "is-ci": "^3.0.1",
35
+ "is-unicode-supported": "^2.0.0",
34
36
  "kleur": "^4.1.5",
35
37
  "strip-ansi": "^7.1.0"
36
38
  },
@@ -1,6 +1,7 @@
1
1
  // Import Node.js Dependencies
2
2
  import { EOL } from "node:os";
3
3
  import { createInterface } from "node:readline";
4
+ import { Writable } from "node:stream";
4
5
 
5
6
  // Import Third-party Dependencies
6
7
  import stripAnsi from "strip-ansi";
@@ -23,11 +24,23 @@ export class AbstractPrompt {
23
24
  this.message = message;
24
25
  this.history = [];
25
26
  this.agent = PromptAgent.agent();
27
+ this.mute = false;
26
28
 
27
29
  if (this.stdout.isTTY) {
28
30
  this.stdin.setRawMode(true);
29
31
  }
30
- this.rl = createInterface({ input, output });
32
+ this.rl = createInterface({
33
+ input,
34
+ output: new Writable({
35
+ write: (chunk, encoding, callback) => {
36
+ if (!this.mute) {
37
+ this.stdout.write(chunk, encoding);
38
+ }
39
+ callback();
40
+ }
41
+ }),
42
+ terminal: true
43
+ });
31
44
  }
32
45
 
33
46
  write(data) {
@@ -3,6 +3,8 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
+ import stripAnsi from "strip-ansi";
7
+ import wcwidth from "@topcli/wcwidth";
6
8
 
7
9
  // Import Internal Dependencies
8
10
  import { AbstractPrompt } from "./abstract-prompt.js";
@@ -65,7 +67,10 @@ export class ConfirmPrompt extends AbstractPrompt {
65
67
  }
66
68
 
67
69
  #onKeypress(resolve, value, key) {
68
- this.stdout.moveCursor(-this.#getQuestionQuery().length, 0);
70
+ this.stdout.moveCursor(
71
+ -this.stdout.columns,
72
+ -Math.floor(wcwidth(stripAnsi(this.#getQuestionQuery())) / this.stdout.columns)
73
+ );
69
74
  this.stdout.clearScreenDown();
70
75
 
71
76
  if (key.name === "return") {
@@ -92,7 +97,11 @@ export class ConfirmPrompt extends AbstractPrompt {
92
97
  }
93
98
 
94
99
  #onQuestionAnswer() {
95
- this.clearLastLine();
100
+ this.stdout.moveCursor(
101
+ -this.stdout.columns,
102
+ -(Math.floor(wcwidth(stripAnsi(this.#getQuestionQuery())) / this.stdout.columns) || 1)
103
+ );
104
+ this.stdout.clearScreenDown();
96
105
  this.write(`${this.selectedValue ? SYMBOLS.Tick : SYMBOLS.Cross} ${kleur.bold(this.message)}${EOL}`);
97
106
  }
98
107
 
package/src/constants.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Import Third-party Dependencies
2
2
  import kleur from "kleur";
3
3
  import isUnicodeSupported from "is-unicode-supported";
4
+ import isCI from "is-ci";
4
5
 
5
6
  const kMainSymbols = {
6
7
  tick: "✔",
@@ -20,7 +21,7 @@ const kFallbackSymbols = {
20
21
  active: "(+)",
21
22
  inactive: "(-)"
22
23
  };
23
- const kSymbols = isUnicodeSupported() ? kMainSymbols : kFallbackSymbols;
24
+ const kSymbols = isUnicodeSupported() || isCI ? kMainSymbols : kFallbackSymbols;
24
25
  const kPointer = kleur.gray(kSymbols.pointer);
25
26
 
26
27
  export const SYMBOLS = {
@@ -3,16 +3,21 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
+ import stripAnsi from "strip-ansi";
7
+ import wcwidth from "@topcli/wcwidth";
6
8
 
7
9
  // Import Internal Dependencies
8
10
  import { AbstractPrompt } from "./abstract-prompt.js";
9
11
  import { SYMBOLS } from "./constants.js";
10
12
 
11
13
  export class MultiselectPrompt extends AbstractPrompt {
14
+ #boundExitEvent = () => void 0;
15
+ #boundKeyPressEvent = () => void 0;
12
16
  #validators;
13
17
 
14
18
  activeIndex = 0;
15
19
  selectedIndexes = [];
20
+ questionMessage;
16
21
 
17
22
  get choices() {
18
23
  return this.options.choices;
@@ -142,6 +147,70 @@ export class MultiselectPrompt extends AbstractPrompt {
142
147
  this.write(`${prefix}${choices ? ` ${formattedChoice}` : ""}${EOL}`);
143
148
  }
144
149
 
150
+ #onProcessExit() {
151
+ this.stdin.off("keypress", this.#boundKeyPressEvent);
152
+ this.stdout.moveCursor(-this.stdout.columns, 0);
153
+ this.stdout.clearScreenDown();
154
+ this.write(SYMBOLS.ShowCursor);
155
+ }
156
+
157
+ #onKeypress(...args) {
158
+ const [resolve, render, _, key] = args;
159
+
160
+ if (key.name === "up") {
161
+ this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
162
+ render();
163
+ }
164
+ else if (key.name === "down") {
165
+ this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
166
+ render();
167
+ }
168
+ else if (key.name === "a") {
169
+ this.selectedIndexes = this.selectedIndexes.length === this.choices.length ? [] : this.choices.map((_, index) => index);
170
+ render();
171
+ }
172
+ else if (key.name === "space") {
173
+ const isChoiceSelected = this.selectedIndexes.includes(this.activeIndex);
174
+
175
+ if (isChoiceSelected) {
176
+ this.selectedIndexes = this.selectedIndexes.filter((index) => index !== this.activeIndex);
177
+ }
178
+ else {
179
+ this.selectedIndexes.push(this.activeIndex);
180
+ }
181
+
182
+ render();
183
+ }
184
+ else if (key.name === "return") {
185
+ const labels = this.selectedIndexes.map((index) => this.choices[index].label ?? this.choices[index]);
186
+ const values = this.selectedIndexes.map((index) => this.choices[index].value ?? this.choices[index]);
187
+
188
+ for (const validator of this.#validators) {
189
+ if (!validator.validate(values)) {
190
+ const error = validator.error(values);
191
+ render({ error });
192
+
193
+ return;
194
+ }
195
+ }
196
+
197
+ render({ clearRender: true });
198
+
199
+ this.#showAnsweredQuestion(labels.join(", "));
200
+
201
+ this.write(SYMBOLS.ShowCursor);
202
+ this.destroy();
203
+
204
+ this.#onProcessExit();
205
+ process.off("exit", this.#boundExitEvent);
206
+
207
+ resolve(values);
208
+ }
209
+ else {
210
+ render();
211
+ }
212
+ }
213
+
145
214
  async multiselect() {
146
215
  if (this.agent.nextAnswers.length > 0) {
147
216
  const answer = this.agent.nextAnswers.shift();
@@ -170,7 +239,10 @@ export class MultiselectPrompt extends AbstractPrompt {
170
239
  }
171
240
 
172
241
  if (clearRender) {
173
- this.stdout.moveCursor(0, -2);
242
+ const questionLineCount = Math.ceil(
243
+ wcwidth(stripAnsi(this.questionMessage)) / this.stdout.columns
244
+ );
245
+ this.stdout.moveCursor(-this.stdout.columns, -(1 + questionLineCount));
174
246
  this.stdout.clearScreenDown();
175
247
 
176
248
  return;
@@ -188,59 +260,11 @@ export class MultiselectPrompt extends AbstractPrompt {
188
260
  render({ initialRender: true });
189
261
 
190
262
  return new Promise((resolve) => {
191
- const onKeypress = (value, key) => {
192
- if (key.name === "up") {
193
- this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
194
- render();
195
- }
196
- else if (key.name === "down") {
197
- this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
198
- render();
199
- }
200
- else if (key.name === "a") {
201
- this.selectedIndexes = this.selectedIndexes.length === this.choices.length ? [] : this.choices.map((_, index) => index);
202
- render();
203
- }
204
- else if (key.name === "space") {
205
- const isChoiceSelected = this.selectedIndexes.includes(this.activeIndex);
206
-
207
- if (isChoiceSelected) {
208
- this.selectedIndexes = this.selectedIndexes.filter((index) => index !== this.activeIndex);
209
- }
210
- else {
211
- this.selectedIndexes.push(this.activeIndex);
212
- }
213
-
214
- render();
215
- }
216
- else if (key.name === "return") {
217
- const labels = this.selectedIndexes.map((index) => this.choices[index].label ?? this.choices[index]);
218
- const values = this.selectedIndexes.map((index) => this.choices[index].value ?? this.choices[index]);
219
-
220
- for (const validator of this.#validators) {
221
- if (!validator.validate(values)) {
222
- const error = validator.error(values);
223
- render({ error });
224
-
225
- return;
226
- }
227
- }
228
-
229
- this.stdin.off("keypress", onKeypress);
230
-
231
- render({ clearRender: true });
232
-
233
-
234
- this.#showAnsweredQuestion(labels.join(", "));
235
-
236
- this.write(SYMBOLS.ShowCursor);
237
- this.destroy();
238
-
239
- resolve(values);
240
- }
241
- };
263
+ this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve, render);
264
+ this.stdin.on("keypress", this.#boundKeyPressEvent);
242
265
 
243
- this.stdin.on("keypress", onKeypress);
266
+ this.#boundExitEvent = this.#onProcessExit.bind(this);
267
+ process.once("exit", this.#boundExitEvent);
244
268
  });
245
269
  }
246
270
 
@@ -251,7 +275,8 @@ export class MultiselectPrompt extends AbstractPrompt {
251
275
  if (error) {
252
276
  hint += ` ${kleur.red().bold(`[${error}]`)}`;
253
277
  }
278
+ this.questionMessage = `${SYMBOLS.QuestionMark} ${kleur.bold(this.message)} ${hint}`;
254
279
 
255
- this.write(`${SYMBOLS.QuestionMark} ${kleur.bold(this.message)} ${hint}${EOL}`);
280
+ this.write(`${this.questionMessage}${EOL}`);
256
281
  }
257
282
  }
@@ -3,6 +3,8 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
+ import stripAnsi from "strip-ansi";
7
+ import wcwidth from "@topcli/wcwidth";
6
8
 
7
9
  // Import Internal Dependencies
8
10
  import { AbstractPrompt } from "./abstract-prompt.js";
@@ -10,13 +12,15 @@ import { SYMBOLS } from "./constants.js";
10
12
 
11
13
  export class QuestionPrompt extends AbstractPrompt {
12
14
  #validators;
15
+ #secure;
13
16
 
14
17
  constructor(message, options = {}) {
15
18
  const {
16
19
  stdin = process.stdin,
17
20
  stdout = process.stdout,
18
21
  defaultValue,
19
- validators = []
22
+ validators = [],
23
+ secure = false
20
24
  } = options;
21
25
 
22
26
  super(message, stdin, stdout);
@@ -28,6 +32,7 @@ export class QuestionPrompt extends AbstractPrompt {
28
32
  this.defaultValue = defaultValue;
29
33
  this.tip = this.defaultValue ? ` (${this.defaultValue})` : "";
30
34
  this.#validators = validators;
35
+ this.#secure = Boolean(secure);
31
36
  this.questionSuffixError = "";
32
37
  }
33
38
 
@@ -37,9 +42,11 @@ export class QuestionPrompt extends AbstractPrompt {
37
42
 
38
43
  this.rl.question(questionQuery, (answer) => {
39
44
  this.history.push(questionQuery + answer);
45
+ this.mute = false;
40
46
 
41
47
  resolve(answer);
42
48
  });
49
+ this.mute = this.#secure;
43
50
  });
44
51
  }
45
52
 
@@ -54,13 +61,17 @@ export class QuestionPrompt extends AbstractPrompt {
54
61
 
55
62
  #writeAnswer() {
56
63
  const prefix = this.answer ? SYMBOLS.Tick : SYMBOLS.Cross;
57
- const answer = kleur.yellow(this.answer ?? "");
58
-
64
+ const answer = kleur.yellow(this.#secure ? "CONFIDENTIAL" : this.answer ?? "");
59
65
  this.write(`${prefix} ${kleur.bold(this.message)} ${SYMBOLS.Pointer} ${answer}${EOL}`);
60
66
  }
61
67
 
62
68
  #onQuestionAnswer() {
63
- this.clearLastLine();
69
+ const questionLineCount = Math.ceil(
70
+ wcwidth(stripAnsi(this.#getQuestionQuery() + this.answer)) / this.stdout.columns
71
+ );
72
+
73
+ this.stdout.moveCursor(-this.stdout.columns, -questionLineCount);
74
+ this.stdout.clearScreenDown();
64
75
 
65
76
  for (const validator of this.#validators) {
66
77
  if (!validator.validate(this.answer)) {
@@ -3,12 +3,16 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
+ import stripAnsi from "strip-ansi";
7
+ import wcwidth from "@topcli/wcwidth";
6
8
 
7
9
  // Import Internal Dependencies
8
10
  import { AbstractPrompt } from "./abstract-prompt.js";
9
11
  import { SYMBOLS } from "./constants.js";
10
12
 
11
13
  export class SelectPrompt extends AbstractPrompt {
14
+ #boundExitEvent = () => void 0;
15
+ #boundKeyPressEvent = () => void 0;
12
16
  activeIndex = 0;
13
17
 
14
18
  get choices() {
@@ -113,6 +117,46 @@ export class SelectPrompt extends AbstractPrompt {
113
117
  this.write(`${prefix} ${formattedChoice}${EOL}`);
114
118
  }
115
119
 
120
+ #onProcessExit() {
121
+ this.stdin.off("keypress", this.#boundKeyPressEvent);
122
+ this.stdout.moveCursor(-this.stdout.columns, 0);
123
+ this.stdout.clearScreenDown();
124
+ this.write(SYMBOLS.ShowCursor);
125
+ }
126
+
127
+ #onKeypress(...args) {
128
+ const [resolve, render, _, key] = args;
129
+
130
+ if (key.name === "up") {
131
+ this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
132
+ render();
133
+ }
134
+ else if (key.name === "down") {
135
+ this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
136
+ render();
137
+ }
138
+ else if (key.name === "return") {
139
+ render({ clearRender: true });
140
+
141
+ const currentChoice = this.choices[this.activeIndex];
142
+ const value = currentChoice.value ?? currentChoice;
143
+
144
+ if (!this.options.ignoreValues?.includes(value)) {
145
+ this.#showAnsweredQuestion(currentChoice);
146
+ }
147
+
148
+ this.write(SYMBOLS.ShowCursor);
149
+ this.destroy();
150
+
151
+ this.#onProcessExit();
152
+ process.off("exit", this.#boundExitEvent);
153
+ resolve(value);
154
+ }
155
+ else {
156
+ render();
157
+ }
158
+ }
159
+
116
160
  async select() {
117
161
  if (this.agent.nextAnswers.length > 0) {
118
162
  const answer = this.agent.nextAnswers.shift();
@@ -140,7 +184,11 @@ export class SelectPrompt extends AbstractPrompt {
140
184
  }
141
185
 
142
186
  if (clearRender) {
143
- this.stdout.moveCursor(0, -2);
187
+ const questionLineCount = Math.ceil(
188
+ wcwidth(stripAnsi(this.questionMessage)) / this.stdout.columns
189
+ );
190
+
191
+ this.stdout.moveCursor(-this.stdout.columns, -(1 + questionLineCount));
144
192
  this.stdout.clearScreenDown();
145
193
 
146
194
  return;
@@ -152,39 +200,16 @@ export class SelectPrompt extends AbstractPrompt {
152
200
  render({ initialRender: true });
153
201
 
154
202
  return new Promise((resolve) => {
155
- const onKeypress = (value, key) => {
156
- if (key.name === "up") {
157
- this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
158
- render();
159
- }
160
- else if (key.name === "down") {
161
- this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
162
- render();
163
- }
164
- else if (key.name === "return") {
165
- this.stdin.off("keypress", onKeypress);
166
-
167
- render({ clearRender: true });
168
-
169
- const currentChoice = this.choices[this.activeIndex];
170
- const value = currentChoice.value ?? currentChoice;
171
-
172
- if (!this.options.ignoreValues?.includes(value)) {
173
- this.#showAnsweredQuestion(currentChoice);
174
- }
175
-
176
- this.write(SYMBOLS.ShowCursor);
177
- this.destroy();
178
-
179
- resolve(value);
180
- }
181
- };
203
+ this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve, render);
204
+ this.stdin.on("keypress", this.#boundKeyPressEvent);
182
205
 
183
- this.stdin.on("keypress", onKeypress);
206
+ this.#boundExitEvent = this.#onProcessExit.bind(this);
207
+ process.once("exit", this.#boundExitEvent);
184
208
  });
185
209
  }
186
210
 
187
211
  #showQuestion() {
188
- this.write(`${SYMBOLS.QuestionMark} ${kleur.bold(this.message)}${EOL}`);
212
+ this.questionMessage = `${SYMBOLS.QuestionMark} ${kleur.bold(this.message)}`;
213
+ this.write(`${this.questionMessage}${EOL}`);
189
214
  }
190
215
  }