@topcli/prompts 2.4.1 → 3.1.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/LICENSE +13 -13
- package/README.md +387 -275
- package/dist/index.cjs +1166 -1042
- package/dist/index.d.cts +105 -68
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +139 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1175 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -55
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -1039
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
import EventEmitter, { once } from "node:events";
|
|
2
|
+
import { EOL } from "node:os";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import { Writable } from "node:stream";
|
|
5
|
+
import { stripVTControlCharacters, styleText } from "node:util";
|
|
6
|
+
import process$1 from "node:process";
|
|
7
|
+
//#region src/validators.ts
|
|
8
|
+
function required() {
|
|
9
|
+
return { validate: (input) => {
|
|
10
|
+
const isValid = Array.isArray(input) ? input.length > 0 : Boolean(input);
|
|
11
|
+
return isValid ? null : {
|
|
12
|
+
isValid,
|
|
13
|
+
error: "required"
|
|
14
|
+
};
|
|
15
|
+
} };
|
|
16
|
+
}
|
|
17
|
+
function isValid(result) {
|
|
18
|
+
if (typeof result === "object") return result?.isValid !== false;
|
|
19
|
+
if (typeof result === "string") return result.length === 0;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
function isValidTransformation(result) {
|
|
23
|
+
return typeof result === "object" && result.isValid === true;
|
|
24
|
+
}
|
|
25
|
+
function resultError(result) {
|
|
26
|
+
if (typeof result === "object") return result.error;
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/prompt-agent.ts
|
|
31
|
+
const kPrivateInstancier = Symbol("instancier");
|
|
32
|
+
var PromptAgent = class PromptAgent {
|
|
33
|
+
/**
|
|
34
|
+
* The prompts answers queue.
|
|
35
|
+
* When not empty, any prompt will be answered by the first answer in this list.
|
|
36
|
+
*/
|
|
37
|
+
nextAnswers = [];
|
|
38
|
+
/**
|
|
39
|
+
* The shared PromptAgent.
|
|
40
|
+
*/
|
|
41
|
+
static #this;
|
|
42
|
+
static agent() {
|
|
43
|
+
return this.#this ??= new PromptAgent(kPrivateInstancier);
|
|
44
|
+
}
|
|
45
|
+
constructor(instancier) {
|
|
46
|
+
if (instancier !== kPrivateInstancier) throw new Error("Cannot instanciate PromptAgent, use PromptAgent.agent() instead");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Programmatically set the next answer for any prompt (`question()`, `confirm()`, `select()`)
|
|
50
|
+
*
|
|
51
|
+
* This is useful for testing.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```js
|
|
55
|
+
* const promptAgent = PromptAgent.agent();
|
|
56
|
+
* promptAgent.nextAnswer("toto");
|
|
57
|
+
*
|
|
58
|
+
* const input = await question("what is your name?");
|
|
59
|
+
* assert.equal(input, "toto");
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
nextAnswer(value) {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
this.nextAnswers.push(...value);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.nextAnswers.push(value);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/transformers.ts
|
|
72
|
+
function number() {
|
|
73
|
+
return { transform(input) {
|
|
74
|
+
if (input.trim() === "") return {
|
|
75
|
+
isValid: false,
|
|
76
|
+
error: "not a number"
|
|
77
|
+
};
|
|
78
|
+
const parsed = Number(input.replace(",", "."));
|
|
79
|
+
if (Number.isNaN(parsed)) return {
|
|
80
|
+
isValid: false,
|
|
81
|
+
error: "not a number"
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
isValid: true,
|
|
85
|
+
transformed: parsed
|
|
86
|
+
};
|
|
87
|
+
} };
|
|
88
|
+
}
|
|
89
|
+
function integer() {
|
|
90
|
+
return { transform(input) {
|
|
91
|
+
if (input.trim() === "") return {
|
|
92
|
+
isValid: false,
|
|
93
|
+
error: "not an integer"
|
|
94
|
+
};
|
|
95
|
+
const parsed = Number(input);
|
|
96
|
+
if (Number.isNaN(parsed) || !Number.isInteger(parsed)) return {
|
|
97
|
+
isValid: false,
|
|
98
|
+
error: "not an integer"
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
isValid: true,
|
|
102
|
+
transformed: parsed
|
|
103
|
+
};
|
|
104
|
+
} };
|
|
105
|
+
}
|
|
106
|
+
function url() {
|
|
107
|
+
return { transform(input) {
|
|
108
|
+
try {
|
|
109
|
+
return {
|
|
110
|
+
isValid: true,
|
|
111
|
+
transformed: new URL(input)
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = new URL(`https://${input}`);
|
|
116
|
+
if (!parsed.hostname.includes(".") && parsed.hostname !== "localhost") return {
|
|
117
|
+
isValid: false,
|
|
118
|
+
error: "invalid URL"
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
isValid: true,
|
|
122
|
+
transformed: parsed
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
return {
|
|
126
|
+
isValid: false,
|
|
127
|
+
error: "invalid URL"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} };
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/errors/abort.ts
|
|
135
|
+
var AbortError = class extends Error {
|
|
136
|
+
constructor(message) {
|
|
137
|
+
super(message);
|
|
138
|
+
this.name = "AbortError";
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/prompts/abstract.ts
|
|
143
|
+
function kNoopTransformer(input) {
|
|
144
|
+
return input;
|
|
145
|
+
}
|
|
146
|
+
var AbstractPrompt = class AbstractPrompt extends EventEmitter {
|
|
147
|
+
stdin;
|
|
148
|
+
stdout;
|
|
149
|
+
message;
|
|
150
|
+
signal;
|
|
151
|
+
skip;
|
|
152
|
+
history;
|
|
153
|
+
agent;
|
|
154
|
+
transformer = kNoopTransformer;
|
|
155
|
+
rl;
|
|
156
|
+
#signalHandler;
|
|
157
|
+
constructor(options) {
|
|
158
|
+
super();
|
|
159
|
+
if (this.constructor === AbstractPrompt) throw new Error("AbstractPrompt can't be instantiated.");
|
|
160
|
+
const { stdin: input = process.stdin, stdout: output = process.stdout, message, signal, skip = false } = options;
|
|
161
|
+
if (typeof message !== "string") throw new TypeError(`message must be string, ${typeof message} given.`);
|
|
162
|
+
if (!output.isTTY) Object.assign(output, {
|
|
163
|
+
moveCursor: () => void 0,
|
|
164
|
+
clearScreenDown: () => void 0
|
|
165
|
+
});
|
|
166
|
+
this.stdin = input;
|
|
167
|
+
this.stdout = output;
|
|
168
|
+
this.message = message;
|
|
169
|
+
this.signal = signal;
|
|
170
|
+
this.skip = skip;
|
|
171
|
+
this.history = [];
|
|
172
|
+
this.agent = PromptAgent.agent();
|
|
173
|
+
if (this.stdout.isTTY) this.stdin.setRawMode(true);
|
|
174
|
+
this.rl = readline.createInterface({
|
|
175
|
+
input,
|
|
176
|
+
output: new Writable({ write: (chunk, encoding, callback) => {
|
|
177
|
+
if (chunk) {
|
|
178
|
+
const transformed = this.transformer(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
179
|
+
if (transformed !== null) this.stdout.write(transformed, encoding);
|
|
180
|
+
}
|
|
181
|
+
callback();
|
|
182
|
+
} }),
|
|
183
|
+
terminal: true
|
|
184
|
+
});
|
|
185
|
+
if (this.signal) {
|
|
186
|
+
this.#signalHandler = () => {
|
|
187
|
+
this.rl.close();
|
|
188
|
+
for (let i = 0; i < this.history.length; i++) this.clearLastLine();
|
|
189
|
+
this.emit("error", new AbortError("Prompt aborted"));
|
|
190
|
+
};
|
|
191
|
+
if (this.signal.aborted) this.#signalHandler();
|
|
192
|
+
this.signal.addEventListener("abort", this.#signalHandler, { once: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
reset() {
|
|
196
|
+
this.transformer = kNoopTransformer;
|
|
197
|
+
}
|
|
198
|
+
write(data) {
|
|
199
|
+
const formattedData = stripVTControlCharacters(data).replace(EOL, "");
|
|
200
|
+
if (formattedData) this.history.push(formattedData);
|
|
201
|
+
return this.stdout.write(data);
|
|
202
|
+
}
|
|
203
|
+
clearLastLine() {
|
|
204
|
+
const lastLine = this.history.pop();
|
|
205
|
+
if (!lastLine) return;
|
|
206
|
+
const lastLineRows = Math.ceil(stripVTControlCharacters(lastLine).length / this.stdout.columns);
|
|
207
|
+
this.stdout.moveCursor(-this.stdout.columns, -lastLineRows);
|
|
208
|
+
this.stdout.clearScreenDown();
|
|
209
|
+
}
|
|
210
|
+
destroy() {
|
|
211
|
+
this.rl.close();
|
|
212
|
+
if (this.signal) this.signal.removeEventListener("abort", this.#signalHandler);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/utils.ts
|
|
217
|
+
function isSeparator(choice) {
|
|
218
|
+
return typeof choice === "object" && choice !== null && choice.type === "separator";
|
|
219
|
+
}
|
|
220
|
+
const kLenSegmenter = new Intl.Segmenter();
|
|
221
|
+
/**
|
|
222
|
+
* @see https://github.com/sindresorhus/is-unicode-supported
|
|
223
|
+
*/
|
|
224
|
+
function isUnicodeSupported() {
|
|
225
|
+
if (process$1.platform !== "win32") return process$1.env.TERM !== "linux";
|
|
226
|
+
return Boolean(process$1.env.WT_SESSION) || Boolean(process$1.env.TERMINUS_SUBLIME) || process$1.env.ConEmuTask === "{cmd::Cmder}" || process$1.env.TERM_PROGRAM === "Terminus-Sublime" || process$1.env.TERM_PROGRAM === "vscode" || process$1.env.TERM === "xterm-256color" || process$1.env.TERM === "alacritty" || process$1.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
227
|
+
}
|
|
228
|
+
function stringLength(string) {
|
|
229
|
+
if (string === "") return 0;
|
|
230
|
+
let length = 0;
|
|
231
|
+
for (const _ of kLenSegmenter.segment(stripVTControlCharacters(string))) length++;
|
|
232
|
+
return length;
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/constants.ts
|
|
236
|
+
const kSymbols = isUnicodeSupported() || process.env.CI ? {
|
|
237
|
+
tick: "✔",
|
|
238
|
+
cross: "✖",
|
|
239
|
+
pointer: "›",
|
|
240
|
+
previous: "⭡",
|
|
241
|
+
next: "⭣",
|
|
242
|
+
active: "●",
|
|
243
|
+
inactive: "○",
|
|
244
|
+
separator: "─"
|
|
245
|
+
} : {
|
|
246
|
+
tick: "√",
|
|
247
|
+
cross: "×",
|
|
248
|
+
pointer: ">",
|
|
249
|
+
previous: "↑",
|
|
250
|
+
next: "↓",
|
|
251
|
+
active: "(+)",
|
|
252
|
+
inactive: "(-)",
|
|
253
|
+
separator: "-"
|
|
254
|
+
};
|
|
255
|
+
const kPointer = styleText("gray", kSymbols.pointer);
|
|
256
|
+
const SYMBOLS = {
|
|
257
|
+
QuestionMark: styleText(["blue", "bold"], "?"),
|
|
258
|
+
Tick: styleText(["green", "bold"], kSymbols.tick),
|
|
259
|
+
Cross: styleText(["red", "bold"], kSymbols.cross),
|
|
260
|
+
Pointer: kPointer,
|
|
261
|
+
Previous: kSymbols.previous,
|
|
262
|
+
Next: kSymbols.next,
|
|
263
|
+
ShowCursor: "\x1B[?25h",
|
|
264
|
+
HideCursor: "\x1B[?25l",
|
|
265
|
+
Active: styleText("cyan", kSymbols.active),
|
|
266
|
+
Inactive: styleText("gray", kSymbols.inactive),
|
|
267
|
+
SeparatorLine: kSymbols.separator
|
|
268
|
+
};
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/prompts/question.ts
|
|
271
|
+
var QuestionPrompt = class extends AbstractPrompt {
|
|
272
|
+
defaultValue;
|
|
273
|
+
tip;
|
|
274
|
+
questionSuffixError;
|
|
275
|
+
answer;
|
|
276
|
+
answerBuffer;
|
|
277
|
+
#validators;
|
|
278
|
+
#transformer;
|
|
279
|
+
#transformedAnswer;
|
|
280
|
+
#secure;
|
|
281
|
+
#securePlaceholder = null;
|
|
282
|
+
constructor(options) {
|
|
283
|
+
const { defaultValue, validators = [], transformer, secure = false, ...baseOptions } = options;
|
|
284
|
+
super({ ...baseOptions });
|
|
285
|
+
if (validators.length > 0 && transformer !== void 0) throw new Error("validators and transformer are mutually exclusive");
|
|
286
|
+
if (defaultValue && typeof defaultValue !== "string") throw new TypeError("defaultValue must be a string");
|
|
287
|
+
this.defaultValue = defaultValue;
|
|
288
|
+
this.tip = this.defaultValue ? ` (${this.defaultValue})` : "";
|
|
289
|
+
this.#validators = validators;
|
|
290
|
+
this.#transformer = transformer;
|
|
291
|
+
if (typeof secure === "object") {
|
|
292
|
+
this.#secure = true;
|
|
293
|
+
this.#securePlaceholder = secure.placeholder;
|
|
294
|
+
} else this.#secure = Boolean(secure);
|
|
295
|
+
this.questionSuffixError = "";
|
|
296
|
+
}
|
|
297
|
+
#question() {
|
|
298
|
+
const { resolve, promise } = Promise.withResolvers();
|
|
299
|
+
const questionQuery = this.#getQuestionQuery();
|
|
300
|
+
this.history.push(questionQuery);
|
|
301
|
+
this.rl.question(questionQuery, (answer) => {
|
|
302
|
+
this.history.push(questionQuery + answer);
|
|
303
|
+
this.reset();
|
|
304
|
+
resolve(answer);
|
|
305
|
+
});
|
|
306
|
+
if (this.#securePlaceholder !== null) this.transformer = (input) => Buffer.from(this.#securePlaceholder.repeat(input.length), "utf-8");
|
|
307
|
+
return promise;
|
|
308
|
+
}
|
|
309
|
+
#getQuestionQuery() {
|
|
310
|
+
return `${styleText("bold", `${SYMBOLS.QuestionMark} ${this.message}${this.tip}`)} ${this.questionSuffixError}`;
|
|
311
|
+
}
|
|
312
|
+
#setQuestionSuffixError(error) {
|
|
313
|
+
this.questionSuffixError = styleText("red", `[${error}] `);
|
|
314
|
+
}
|
|
315
|
+
#writeAnswer() {
|
|
316
|
+
const prefix = this.answer ? SYMBOLS.Tick : SYMBOLS.Cross;
|
|
317
|
+
const answer = this.answer ?? "";
|
|
318
|
+
const maskedAnswer = this.#securePlaceholder ? this.#securePlaceholder.repeat(answer.length) : answer;
|
|
319
|
+
const stylizedAnswer = styleText("yellow", this.#secure && this.#securePlaceholder === null ? "CONFIDENTIAL" : maskedAnswer);
|
|
320
|
+
this.write(`${prefix} ${styleText("bold", this.message)} ${SYMBOLS.Pointer} ${stylizedAnswer}${EOL}`);
|
|
321
|
+
}
|
|
322
|
+
#getValidatingQuery(dotCount) {
|
|
323
|
+
return `${styleText("bold", `${SYMBOLS.QuestionMark} ${this.message}${this.tip}`)} ${styleText("yellow", `[validating${".".repeat(dotCount)}]`)}${EOL}`;
|
|
324
|
+
}
|
|
325
|
+
async #runTransformer() {
|
|
326
|
+
const result = this.#transformer.transform(this.answer);
|
|
327
|
+
if (result instanceof Promise) {
|
|
328
|
+
let dotCount = 1;
|
|
329
|
+
this.write(this.#getValidatingQuery(dotCount));
|
|
330
|
+
const spinnerInterval = setInterval(() => {
|
|
331
|
+
dotCount = dotCount % 3 + 1;
|
|
332
|
+
this.clearLastLine();
|
|
333
|
+
this.write(this.#getValidatingQuery(dotCount));
|
|
334
|
+
}, 300);
|
|
335
|
+
try {
|
|
336
|
+
return await result;
|
|
337
|
+
} finally {
|
|
338
|
+
clearInterval(spinnerInterval);
|
|
339
|
+
this.clearLastLine();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
async #onQuestionAnswer() {
|
|
345
|
+
const questionLineCount = Math.ceil(stringLength(this.#getQuestionQuery() + this.answer) / this.stdout.columns);
|
|
346
|
+
this.stdout.moveCursor(-this.stdout.columns, -questionLineCount);
|
|
347
|
+
this.stdout.clearScreenDown();
|
|
348
|
+
if (this.#transformer) {
|
|
349
|
+
const result = await this.#runTransformer();
|
|
350
|
+
if (isValidTransformation(result) === false) {
|
|
351
|
+
this.#setQuestionSuffixError(resultError(result));
|
|
352
|
+
this.answerBuffer = this.#question();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
this.#transformedAnswer = result.transformed;
|
|
356
|
+
this.answerBuffer = void 0;
|
|
357
|
+
this.#writeAnswer();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
for (const validator of this.#validators) {
|
|
361
|
+
let validationResult;
|
|
362
|
+
const result = validator.validate(this.answer);
|
|
363
|
+
if (result instanceof Promise) {
|
|
364
|
+
let dotCount = 1;
|
|
365
|
+
this.write(this.#getValidatingQuery(dotCount));
|
|
366
|
+
const spinnerInterval = setInterval(() => {
|
|
367
|
+
dotCount = dotCount % 3 + 1;
|
|
368
|
+
this.clearLastLine();
|
|
369
|
+
this.write(this.#getValidatingQuery(dotCount));
|
|
370
|
+
}, 300);
|
|
371
|
+
try {
|
|
372
|
+
validationResult = await result;
|
|
373
|
+
} finally {
|
|
374
|
+
clearInterval(spinnerInterval);
|
|
375
|
+
this.clearLastLine();
|
|
376
|
+
}
|
|
377
|
+
} else validationResult = result;
|
|
378
|
+
if (isValid(validationResult) === false) {
|
|
379
|
+
this.#setQuestionSuffixError(resultError(validationResult));
|
|
380
|
+
this.answerBuffer = this.#question();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
this.answerBuffer = void 0;
|
|
385
|
+
this.#writeAnswer();
|
|
386
|
+
}
|
|
387
|
+
async listen() {
|
|
388
|
+
if (this.skip) {
|
|
389
|
+
this.destroy();
|
|
390
|
+
if (this.#transformer) {
|
|
391
|
+
const rawValue = this.defaultValue ?? "";
|
|
392
|
+
const result = await this.#transformer.transform(rawValue);
|
|
393
|
+
if (isValidTransformation(result) === false) throw new Error(`Transformer failed for default value "${rawValue}": ${resultError(result)}`);
|
|
394
|
+
return result.transformed;
|
|
395
|
+
}
|
|
396
|
+
return this.defaultValue ?? "";
|
|
397
|
+
}
|
|
398
|
+
const agentAnswer = this.agent.nextAnswers.shift();
|
|
399
|
+
if (agentAnswer !== void 0) {
|
|
400
|
+
this.answer = agentAnswer;
|
|
401
|
+
this.#writeAnswer();
|
|
402
|
+
this.destroy();
|
|
403
|
+
if (this.#transformer) {
|
|
404
|
+
const result = await this.#transformer.transform(agentAnswer);
|
|
405
|
+
if (isValidTransformation(result) === false) throw new Error(`(PromptAgent) transformer failed for answer "${agentAnswer}": ${resultError(result)}`);
|
|
406
|
+
return result.transformed;
|
|
407
|
+
}
|
|
408
|
+
return this.answer;
|
|
409
|
+
}
|
|
410
|
+
this.answer = await this.#question();
|
|
411
|
+
if (this.answer === "" && this.defaultValue) this.answer = this.defaultValue;
|
|
412
|
+
await this.#onQuestionAnswer();
|
|
413
|
+
while (this.answerBuffer !== void 0) {
|
|
414
|
+
this.answer = await this.answerBuffer;
|
|
415
|
+
await this.#onQuestionAnswer();
|
|
416
|
+
}
|
|
417
|
+
this.destroy();
|
|
418
|
+
return this.#transformedAnswer ?? this.answer;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/prompts/confirm.ts
|
|
423
|
+
const kToggleKeys = new Set([
|
|
424
|
+
"left",
|
|
425
|
+
"right",
|
|
426
|
+
"tab",
|
|
427
|
+
"q",
|
|
428
|
+
"a",
|
|
429
|
+
"d",
|
|
430
|
+
"h",
|
|
431
|
+
"j",
|
|
432
|
+
"k",
|
|
433
|
+
"l",
|
|
434
|
+
"space"
|
|
435
|
+
]);
|
|
436
|
+
var ConfirmPrompt = class extends AbstractPrompt {
|
|
437
|
+
initial;
|
|
438
|
+
selectedValue;
|
|
439
|
+
fastAnswer;
|
|
440
|
+
#boundKeyPressEvent;
|
|
441
|
+
#boundExitEvent;
|
|
442
|
+
constructor(options) {
|
|
443
|
+
const { initial = false, ...baseOptions } = options;
|
|
444
|
+
super({ ...baseOptions });
|
|
445
|
+
this.initial = initial;
|
|
446
|
+
this.selectedValue = initial;
|
|
447
|
+
}
|
|
448
|
+
#getHint() {
|
|
449
|
+
const Yes = styleText([
|
|
450
|
+
"cyan",
|
|
451
|
+
"bold",
|
|
452
|
+
"underline"
|
|
453
|
+
], "Yes");
|
|
454
|
+
const No = styleText([
|
|
455
|
+
"cyan",
|
|
456
|
+
"bold",
|
|
457
|
+
"underline"
|
|
458
|
+
], "No");
|
|
459
|
+
return this.selectedValue ? `${Yes}/No` : `Yes/${No}`;
|
|
460
|
+
}
|
|
461
|
+
#render() {
|
|
462
|
+
this.write(this.#getQuestionQuery());
|
|
463
|
+
}
|
|
464
|
+
#onKeypress(resolve, _value, key) {
|
|
465
|
+
this.stdout.moveCursor(-this.stdout.columns, -Math.floor(stringLength(this.#getQuestionQuery()) / this.stdout.columns));
|
|
466
|
+
this.stdout.clearScreenDown();
|
|
467
|
+
if (key.name === "return") {
|
|
468
|
+
resolve(this.selectedValue);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (kToggleKeys.has(key.name ?? "")) this.selectedValue = !this.selectedValue;
|
|
472
|
+
if (key.name === "y") {
|
|
473
|
+
this.selectedValue = true;
|
|
474
|
+
resolve(true);
|
|
475
|
+
this.fastAnswer = true;
|
|
476
|
+
} else if (key.name === "n") {
|
|
477
|
+
this.selectedValue = false;
|
|
478
|
+
resolve(false);
|
|
479
|
+
this.fastAnswer = true;
|
|
480
|
+
}
|
|
481
|
+
if (!this.fastAnswer) this.#render();
|
|
482
|
+
}
|
|
483
|
+
#onProcessExit() {
|
|
484
|
+
this.stdin.off("keypress", this.#boundKeyPressEvent);
|
|
485
|
+
}
|
|
486
|
+
#getQuestionQuery() {
|
|
487
|
+
return `${styleText("bold", `${SYMBOLS.QuestionMark} ${this.message}`)} ${this.#getHint()}`;
|
|
488
|
+
}
|
|
489
|
+
#onQuestionAnswer() {
|
|
490
|
+
this.clearLastLine();
|
|
491
|
+
this.stdout.moveCursor(-this.stdout.columns, -Math.floor(stringLength(this.#getQuestionQuery()) / this.stdout.columns));
|
|
492
|
+
this.stdout.clearScreenDown();
|
|
493
|
+
this.write(`${this.selectedValue ? SYMBOLS.Tick : SYMBOLS.Cross} ${styleText("bold", this.message)}${EOL}`);
|
|
494
|
+
}
|
|
495
|
+
async listen() {
|
|
496
|
+
if (this.skip) {
|
|
497
|
+
this.destroy();
|
|
498
|
+
return this.initial;
|
|
499
|
+
}
|
|
500
|
+
const answer = this.agent.nextAnswers.shift();
|
|
501
|
+
if (answer !== void 0) {
|
|
502
|
+
this.selectedValue = answer;
|
|
503
|
+
this.#onQuestionAnswer();
|
|
504
|
+
this.destroy();
|
|
505
|
+
return answer;
|
|
506
|
+
}
|
|
507
|
+
this.write(SYMBOLS.HideCursor);
|
|
508
|
+
try {
|
|
509
|
+
const { resolve, promise } = Promise.withResolvers();
|
|
510
|
+
const questionQuery = this.#getQuestionQuery();
|
|
511
|
+
this.write(questionQuery);
|
|
512
|
+
this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve);
|
|
513
|
+
this.stdin.on("keypress", this.#boundKeyPressEvent);
|
|
514
|
+
this.#boundExitEvent = this.#onProcessExit.bind(this);
|
|
515
|
+
process.once("exit", this.#boundExitEvent);
|
|
516
|
+
await promise;
|
|
517
|
+
this.#onQuestionAnswer();
|
|
518
|
+
} finally {
|
|
519
|
+
this.write(SYMBOLS.ShowCursor);
|
|
520
|
+
this.#onProcessExit();
|
|
521
|
+
process.off("exit", this.#boundExitEvent);
|
|
522
|
+
this.destroy();
|
|
523
|
+
}
|
|
524
|
+
return this.selectedValue;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/prompts/select.ts
|
|
529
|
+
const kRequiredChoiceProperties$1 = ["label", "value"];
|
|
530
|
+
var SelectPrompt = class extends AbstractPrompt {
|
|
531
|
+
#boundExitEvent = () => void 0;
|
|
532
|
+
#boundKeyPressEvent = () => void 0;
|
|
533
|
+
#validators;
|
|
534
|
+
#isValidating = false;
|
|
535
|
+
activeIndex = 0;
|
|
536
|
+
questionMessage;
|
|
537
|
+
autocompleteValue = "";
|
|
538
|
+
options;
|
|
539
|
+
lastRender;
|
|
540
|
+
get choices() {
|
|
541
|
+
return this.options.choices;
|
|
542
|
+
}
|
|
543
|
+
get filteredChoices() {
|
|
544
|
+
if (!(this.options.autocomplete && this.autocompleteValue.length > 0)) return this.choices;
|
|
545
|
+
const isCaseSensitive = this.options.caseSensitive;
|
|
546
|
+
const autocompleteValue = isCaseSensitive ? this.autocompleteValue : this.autocompleteValue.toLowerCase();
|
|
547
|
+
return this.choices.filter((choice) => !isSeparator(choice) && this.#filterChoice(choice, autocompleteValue, isCaseSensitive));
|
|
548
|
+
}
|
|
549
|
+
#filterChoice(choice, autocompleteValue, isCaseSensitive = false) {
|
|
550
|
+
const choiceValue = typeof choice === "string" ? isCaseSensitive ? choice : choice.toLowerCase() : isCaseSensitive ? choice.label : choice.label.toLowerCase();
|
|
551
|
+
if (autocompleteValue.includes(" ")) return this.#filterMultipleWords(choiceValue, autocompleteValue, isCaseSensitive);
|
|
552
|
+
return choiceValue.includes(autocompleteValue);
|
|
553
|
+
}
|
|
554
|
+
#filterMultipleWords(choiceValue, autocompleteValue, isCaseSensitive) {
|
|
555
|
+
return autocompleteValue.split(" ").every((word) => {
|
|
556
|
+
const wordValue = isCaseSensitive ? word : word.toLowerCase();
|
|
557
|
+
return choiceValue.includes(wordValue) || choiceValue.includes(autocompleteValue);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
#isChoiceDisabled(choice) {
|
|
561
|
+
return typeof choice !== "string" && Boolean(choice.disabled);
|
|
562
|
+
}
|
|
563
|
+
#findNextEnabledIndex(from, direction) {
|
|
564
|
+
const total = this.filteredChoices.length;
|
|
565
|
+
if (total === 0) return from;
|
|
566
|
+
let index = (from + direction + total) % total;
|
|
567
|
+
while (index !== from) {
|
|
568
|
+
const choice = this.filteredChoices[index];
|
|
569
|
+
if (!isSeparator(choice) && !this.#isChoiceDisabled(choice)) return index;
|
|
570
|
+
index = (index + direction + total) % total;
|
|
571
|
+
}
|
|
572
|
+
return from;
|
|
573
|
+
}
|
|
574
|
+
#findFirstEnabledIndex() {
|
|
575
|
+
const index = this.filteredChoices.findIndex((choice) => !isSeparator(choice) && !this.#isChoiceDisabled(choice));
|
|
576
|
+
return index === -1 ? 0 : index;
|
|
577
|
+
}
|
|
578
|
+
get longestChoice() {
|
|
579
|
+
const selectableChoices = this.filteredChoices.filter((choice) => !isSeparator(choice));
|
|
580
|
+
if (selectableChoices.length === 0) return 0;
|
|
581
|
+
return Math.max(...selectableChoices.map((choice) => {
|
|
582
|
+
if (typeof choice === "string") return choice.length;
|
|
583
|
+
return choice.label.length;
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
constructor(options) {
|
|
587
|
+
const { choices, validators = [], ...baseOptions } = options;
|
|
588
|
+
super({ ...baseOptions });
|
|
589
|
+
this.options = options;
|
|
590
|
+
if (!choices?.length) {
|
|
591
|
+
this.destroy();
|
|
592
|
+
throw new TypeError("Missing required param: choices");
|
|
593
|
+
}
|
|
594
|
+
this.#validators = validators;
|
|
595
|
+
for (const choice of choices) {
|
|
596
|
+
if (typeof choice === "string" || isSeparator(choice)) continue;
|
|
597
|
+
for (const prop of kRequiredChoiceProperties$1) if (!choice[prop]) {
|
|
598
|
+
this.destroy();
|
|
599
|
+
throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (choices.findIndex((choice) => !isSeparator(choice)) === -1) {
|
|
603
|
+
this.destroy();
|
|
604
|
+
throw new TypeError("choices must contain at least one non-separator item");
|
|
605
|
+
}
|
|
606
|
+
this.activeIndex = this.#findFirstEnabledIndex();
|
|
607
|
+
}
|
|
608
|
+
#getFormattedChoice(choiceIndex) {
|
|
609
|
+
const choice = this.filteredChoices[choiceIndex];
|
|
610
|
+
if (typeof choice === "string") return {
|
|
611
|
+
value: choice,
|
|
612
|
+
label: choice
|
|
613
|
+
};
|
|
614
|
+
return choice;
|
|
615
|
+
}
|
|
616
|
+
#getVisibleChoices() {
|
|
617
|
+
const maxVisible = this.options.maxVisible || 8;
|
|
618
|
+
let startIndex = Math.min(this.filteredChoices.length - maxVisible, this.activeIndex - Math.floor(maxVisible / 2));
|
|
619
|
+
if (startIndex < 0) startIndex = 0;
|
|
620
|
+
const endIndex = Math.min(startIndex + maxVisible, this.filteredChoices.length);
|
|
621
|
+
return {
|
|
622
|
+
startIndex,
|
|
623
|
+
endIndex
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
#showChoices() {
|
|
627
|
+
const { startIndex, endIndex } = this.#getVisibleChoices();
|
|
628
|
+
this.lastRender = {
|
|
629
|
+
startIndex,
|
|
630
|
+
endIndex
|
|
631
|
+
};
|
|
632
|
+
if (this.options.autocomplete) this.write(`${SYMBOLS.Pointer} ${this.autocompleteValue}${EOL}`);
|
|
633
|
+
for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) {
|
|
634
|
+
const rawChoice = this.filteredChoices[choiceIndex];
|
|
635
|
+
if (isSeparator(rawChoice)) {
|
|
636
|
+
const separatorLabel = rawChoice.label ? ` ${rawChoice.label} ` : "";
|
|
637
|
+
this.write(` ${styleText("gray", `${SYMBOLS.SeparatorLine}${SYMBOLS.SeparatorLine}${separatorLabel}${SYMBOLS.SeparatorLine}${SYMBOLS.SeparatorLine}`)}${EOL}`);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const formattedChoice = this.#getFormattedChoice(choiceIndex);
|
|
641
|
+
const isChoiceSelected = choiceIndex === this.activeIndex;
|
|
642
|
+
const isChoiceDisabled = this.#isChoiceDisabled(rawChoice);
|
|
643
|
+
const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex;
|
|
644
|
+
const showNextChoicesArrow = endIndex < this.filteredChoices.length && choiceIndex === endIndex - 1;
|
|
645
|
+
let prefixArrow = " ";
|
|
646
|
+
if (showPreviousChoicesArrow) prefixArrow = SYMBOLS.Previous;
|
|
647
|
+
else if (showNextChoicesArrow) prefixArrow = SYMBOLS.Next;
|
|
648
|
+
const prefix = isChoiceDisabled ? `${prefixArrow} ` : `${prefixArrow}${isChoiceSelected ? `${SYMBOLS.Pointer} ` : " "}`;
|
|
649
|
+
const formattedLabel = formattedChoice.label.padEnd(this.longestChoice < 10 ? this.longestChoice : 0);
|
|
650
|
+
const formattedDescription = formattedChoice.description ? ` - ${formattedChoice.description}` : "";
|
|
651
|
+
const disabledMessage = isChoiceDisabled && typeof rawChoice !== "string" && typeof rawChoice.disabled === "string" ? ` [${rawChoice.disabled}]` : "";
|
|
652
|
+
let textStyles;
|
|
653
|
+
if (isChoiceDisabled) textStyles = ["gray", "dim"];
|
|
654
|
+
else if (isChoiceSelected) textStyles = ["white", "bold"];
|
|
655
|
+
else textStyles = ["gray"];
|
|
656
|
+
const str = `${prefix}${styleText(textStyles, `${formattedLabel}${formattedDescription}${disabledMessage}`)}${EOL}`;
|
|
657
|
+
this.write(str);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async #handleReturn(resolve, render) {
|
|
661
|
+
const activeChoice = this.filteredChoices[this.activeIndex];
|
|
662
|
+
if (isSeparator(activeChoice)) return;
|
|
663
|
+
if (activeChoice !== void 0 && this.#isChoiceDisabled(activeChoice)) return;
|
|
664
|
+
this.#isValidating = true;
|
|
665
|
+
try {
|
|
666
|
+
const choice = activeChoice ?? "";
|
|
667
|
+
const label = typeof choice === "string" ? choice : choice.label;
|
|
668
|
+
const value = typeof choice === "string" ? choice : choice.value;
|
|
669
|
+
for (const validator of this.#validators) {
|
|
670
|
+
let validationResult;
|
|
671
|
+
const result = validator.validate(value);
|
|
672
|
+
if (result instanceof Promise) {
|
|
673
|
+
let dotCount = 1;
|
|
674
|
+
render({ validating: `validating${".".repeat(dotCount)}` });
|
|
675
|
+
const spinnerInterval = setInterval(() => {
|
|
676
|
+
dotCount = dotCount % 3 + 1;
|
|
677
|
+
render({ validating: `validating${".".repeat(dotCount)}` });
|
|
678
|
+
}, 300);
|
|
679
|
+
try {
|
|
680
|
+
validationResult = await result;
|
|
681
|
+
} finally {
|
|
682
|
+
clearInterval(spinnerInterval);
|
|
683
|
+
}
|
|
684
|
+
} else validationResult = result;
|
|
685
|
+
if (isValid(validationResult) === false) {
|
|
686
|
+
render({ error: resultError(validationResult) });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
render({ clearRender: true });
|
|
691
|
+
if (!this.options.ignoreValues?.includes(value)) this.#showAnsweredQuestion(label);
|
|
692
|
+
this.write(SYMBOLS.ShowCursor);
|
|
693
|
+
this.destroy();
|
|
694
|
+
this.#onProcessExit();
|
|
695
|
+
process.off("exit", this.#boundExitEvent);
|
|
696
|
+
resolve(value);
|
|
697
|
+
} finally {
|
|
698
|
+
this.#isValidating = false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
#showAnsweredQuestion(label) {
|
|
702
|
+
const prefix = `${label === "" ? SYMBOLS.Cross : SYMBOLS.Tick} ${styleText("bold", this.message)} ${SYMBOLS.Pointer}`;
|
|
703
|
+
const formattedChoice = styleText("yellow", label);
|
|
704
|
+
this.write(`${prefix} ${formattedChoice}${EOL}`);
|
|
705
|
+
}
|
|
706
|
+
#onProcessExit() {
|
|
707
|
+
this.stdin.off("keypress", this.#boundKeyPressEvent);
|
|
708
|
+
this.stdout.moveCursor(-this.stdout.columns, 0);
|
|
709
|
+
this.stdout.clearScreenDown();
|
|
710
|
+
this.write(SYMBOLS.ShowCursor);
|
|
711
|
+
}
|
|
712
|
+
#onKeypress(...args) {
|
|
713
|
+
const [resolve, render, , key] = args;
|
|
714
|
+
if (this.#isValidating) return;
|
|
715
|
+
if (key.name === "up") {
|
|
716
|
+
this.activeIndex = this.#findNextEnabledIndex(this.activeIndex, -1);
|
|
717
|
+
render();
|
|
718
|
+
} else if (key.name === "down") {
|
|
719
|
+
this.activeIndex = this.#findNextEnabledIndex(this.activeIndex, 1);
|
|
720
|
+
render();
|
|
721
|
+
} else if (key.name === "return") this.#handleReturn(resolve, render);
|
|
722
|
+
else {
|
|
723
|
+
if (!key.ctrl && this.options.autocomplete) {
|
|
724
|
+
this.activeIndex = this.#findFirstEnabledIndex();
|
|
725
|
+
if (key.name === "backspace" && this.autocompleteValue.length > 0) this.autocompleteValue = this.autocompleteValue.slice(0, -1);
|
|
726
|
+
else if (key.name !== "backspace") this.autocompleteValue += key.sequence;
|
|
727
|
+
}
|
|
728
|
+
render();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async listen() {
|
|
732
|
+
if (this.skip) {
|
|
733
|
+
this.destroy();
|
|
734
|
+
const firstSelectable = this.filteredChoices.find((choice) => !isSeparator(choice));
|
|
735
|
+
return typeof firstSelectable === "string" ? firstSelectable : firstSelectable?.value ?? "";
|
|
736
|
+
}
|
|
737
|
+
const answer = this.agent.nextAnswers.shift();
|
|
738
|
+
if (answer !== void 0) {
|
|
739
|
+
this.#showAnsweredQuestion(answer);
|
|
740
|
+
this.destroy();
|
|
741
|
+
return answer;
|
|
742
|
+
}
|
|
743
|
+
this.transformer = () => null;
|
|
744
|
+
this.write(SYMBOLS.HideCursor);
|
|
745
|
+
this.#showQuestion();
|
|
746
|
+
const render = (options = {}) => {
|
|
747
|
+
const { initialRender = false, clearRender = false, error = null, validating = null } = options;
|
|
748
|
+
if (!initialRender) {
|
|
749
|
+
let linesToClear = this.lastRender.endIndex - this.lastRender.startIndex;
|
|
750
|
+
while (linesToClear > 0) {
|
|
751
|
+
this.clearLastLine();
|
|
752
|
+
linesToClear--;
|
|
753
|
+
}
|
|
754
|
+
if (this.options.autocomplete) {
|
|
755
|
+
let linesToClear = Math.ceil(stringLength(`${SYMBOLS.Pointer} ${this.autocompleteValue}`) / this.stdout.columns);
|
|
756
|
+
while (linesToClear > 0) {
|
|
757
|
+
this.clearLastLine();
|
|
758
|
+
linesToClear--;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (clearRender) {
|
|
763
|
+
this.clearLastLine();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (error || validating) {
|
|
767
|
+
this.clearLastLine();
|
|
768
|
+
this.#showQuestion(error, validating);
|
|
769
|
+
}
|
|
770
|
+
this.#showChoices();
|
|
771
|
+
};
|
|
772
|
+
render({ initialRender: true });
|
|
773
|
+
this.#boundExitEvent = this.#onProcessExit.bind(this);
|
|
774
|
+
process.once("exit", this.#boundExitEvent);
|
|
775
|
+
const { resolve, promise } = Promise.withResolvers();
|
|
776
|
+
this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve, render);
|
|
777
|
+
this.stdin.on("keypress", this.#boundKeyPressEvent);
|
|
778
|
+
return promise;
|
|
779
|
+
}
|
|
780
|
+
#showQuestion(error = null, validating = null) {
|
|
781
|
+
let hint = "";
|
|
782
|
+
if (validating) hint = styleText("yellow", `[${validating}]`);
|
|
783
|
+
else if (error) hint += `${hint.length > 0 ? " " : ""}${styleText(["red", "bold"], `[${error}]`)}`;
|
|
784
|
+
this.questionMessage = `${SYMBOLS.QuestionMark} ${styleText("bold", this.message)}${hint.length > 0 ? ` ${hint}` : ""}`;
|
|
785
|
+
this.write(`${this.questionMessage}${EOL}`);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/prompts/multiselect.ts
|
|
790
|
+
const kRequiredChoiceProperties = ["label", "value"];
|
|
791
|
+
var MultiselectPrompt = class extends AbstractPrompt {
|
|
792
|
+
#boundExitEvent = () => void 0;
|
|
793
|
+
#boundKeyPressEvent = () => void 0;
|
|
794
|
+
#validators;
|
|
795
|
+
#showHint;
|
|
796
|
+
#isValidating = false;
|
|
797
|
+
activeIndex = 0;
|
|
798
|
+
selectedIndexes = /* @__PURE__ */ new Set();
|
|
799
|
+
questionMessage;
|
|
800
|
+
autocompleteValue = "";
|
|
801
|
+
options;
|
|
802
|
+
lastRender;
|
|
803
|
+
get choices() {
|
|
804
|
+
return this.options.choices;
|
|
805
|
+
}
|
|
806
|
+
get filteredChoices() {
|
|
807
|
+
if (!(this.options.autocomplete && this.autocompleteValue.length > 0)) return this.choices;
|
|
808
|
+
const isCaseSensitive = this.options.caseSensitive;
|
|
809
|
+
const autocompleteValue = isCaseSensitive ? this.autocompleteValue : this.autocompleteValue.toLowerCase();
|
|
810
|
+
return this.choices.filter((choice) => !isSeparator(choice) && this.#filterChoice(choice, autocompleteValue, isCaseSensitive));
|
|
811
|
+
}
|
|
812
|
+
#filterChoice(choice, autocompleteValue, isCaseSensitive = false) {
|
|
813
|
+
const choiceValue = typeof choice === "string" ? isCaseSensitive ? choice : choice.toLowerCase() : isCaseSensitive ? choice.label : choice.label.toLowerCase();
|
|
814
|
+
if (autocompleteValue.includes(" ")) return this.#filterMultipleWords(choiceValue, autocompleteValue, isCaseSensitive);
|
|
815
|
+
return choiceValue.includes(autocompleteValue);
|
|
816
|
+
}
|
|
817
|
+
#filterMultipleWords(choiceValue, autocompleteValue, isCaseSensitive) {
|
|
818
|
+
return autocompleteValue.split(" ").every((word) => {
|
|
819
|
+
const wordValue = isCaseSensitive ? word : word.toLowerCase();
|
|
820
|
+
return choiceValue.includes(wordValue) || choiceValue.includes(autocompleteValue);
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
#isChoiceDisabled(choice) {
|
|
824
|
+
return typeof choice !== "string" && Boolean(choice.disabled);
|
|
825
|
+
}
|
|
826
|
+
#findNextEnabledIndex(from, direction) {
|
|
827
|
+
const total = this.filteredChoices.length;
|
|
828
|
+
if (total === 0) return from;
|
|
829
|
+
let index = (from + direction + total) % total;
|
|
830
|
+
while (index !== from) {
|
|
831
|
+
const choice = this.filteredChoices[index];
|
|
832
|
+
if (!isSeparator(choice) && !this.#isChoiceDisabled(choice)) return index;
|
|
833
|
+
index = (index + direction + total) % total;
|
|
834
|
+
}
|
|
835
|
+
return from;
|
|
836
|
+
}
|
|
837
|
+
#findFirstEnabledIndex() {
|
|
838
|
+
const index = this.filteredChoices.findIndex((choice) => !isSeparator(choice) && !this.#isChoiceDisabled(choice));
|
|
839
|
+
return index === -1 ? 0 : index;
|
|
840
|
+
}
|
|
841
|
+
get longestChoice() {
|
|
842
|
+
const selectableChoices = this.filteredChoices.filter((choice) => !isSeparator(choice));
|
|
843
|
+
if (selectableChoices.length === 0) return 0;
|
|
844
|
+
return Math.max(...selectableChoices.map((choice) => {
|
|
845
|
+
if (typeof choice === "string") return choice.length;
|
|
846
|
+
return choice.label.length;
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
constructor(options) {
|
|
850
|
+
const { choices, preSelectedChoices = [], validators = [], showHint = true, ...baseOptions } = options;
|
|
851
|
+
super({ ...baseOptions });
|
|
852
|
+
this.options = options;
|
|
853
|
+
if (!choices?.length) {
|
|
854
|
+
this.destroy();
|
|
855
|
+
throw new TypeError("Missing required param: choices");
|
|
856
|
+
}
|
|
857
|
+
this.#validators = validators;
|
|
858
|
+
this.#showHint = showHint;
|
|
859
|
+
for (const choice of choices) {
|
|
860
|
+
if (typeof choice === "string" || isSeparator(choice)) continue;
|
|
861
|
+
for (const prop of kRequiredChoiceProperties) if (!choice[prop]) {
|
|
862
|
+
this.destroy();
|
|
863
|
+
throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (choices.findIndex((choice) => !isSeparator(choice)) === -1) {
|
|
867
|
+
this.destroy();
|
|
868
|
+
throw new TypeError("choices must contain at least one non-separator item");
|
|
869
|
+
}
|
|
870
|
+
this.activeIndex = this.#findFirstEnabledIndex();
|
|
871
|
+
for (const choice of preSelectedChoices) {
|
|
872
|
+
const choiceIndex = this.filteredChoices.findIndex((item) => {
|
|
873
|
+
if (typeof item === "string") return item === choice;
|
|
874
|
+
if (isSeparator(item)) return false;
|
|
875
|
+
return item.value === (typeof choice === "string" ? choice : choice.value);
|
|
876
|
+
});
|
|
877
|
+
if (choiceIndex === -1) {
|
|
878
|
+
this.destroy();
|
|
879
|
+
throw new Error(`Invalid pre-selected choice: ${typeof choice === "string" ? choice : choice.value}`);
|
|
880
|
+
}
|
|
881
|
+
if (this.#isChoiceDisabled(this.filteredChoices[choiceIndex])) {
|
|
882
|
+
this.destroy();
|
|
883
|
+
throw new Error(`Cannot pre-select a disabled choice: ${typeof choice === "string" ? choice : choice.value}`);
|
|
884
|
+
}
|
|
885
|
+
this.selectedIndexes.add(choiceIndex);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
#getFormattedChoice(choiceIndex) {
|
|
889
|
+
const choice = this.filteredChoices[choiceIndex];
|
|
890
|
+
if (typeof choice === "string") return {
|
|
891
|
+
value: choice,
|
|
892
|
+
label: choice
|
|
893
|
+
};
|
|
894
|
+
return choice;
|
|
895
|
+
}
|
|
896
|
+
#getVisibleChoices() {
|
|
897
|
+
const maxVisible = this.options.maxVisible || 8;
|
|
898
|
+
let startIndex = Math.min(this.filteredChoices.length - maxVisible, this.activeIndex - Math.floor(maxVisible / 2));
|
|
899
|
+
if (startIndex < 0) startIndex = 0;
|
|
900
|
+
const endIndex = Math.min(startIndex + maxVisible, this.filteredChoices.length);
|
|
901
|
+
return {
|
|
902
|
+
startIndex,
|
|
903
|
+
endIndex
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
#showChoices() {
|
|
907
|
+
const { startIndex, endIndex } = this.#getVisibleChoices();
|
|
908
|
+
this.lastRender = {
|
|
909
|
+
startIndex,
|
|
910
|
+
endIndex
|
|
911
|
+
};
|
|
912
|
+
if (this.options.autocomplete) this.write(`${SYMBOLS.Pointer} ${this.autocompleteValue}${EOL}`);
|
|
913
|
+
for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) {
|
|
914
|
+
const rawChoice = this.filteredChoices[choiceIndex];
|
|
915
|
+
if (isSeparator(rawChoice)) {
|
|
916
|
+
const separatorLabel = rawChoice.label ? ` ${rawChoice.label} ` : "";
|
|
917
|
+
this.write(` ${styleText("gray", `${SYMBOLS.SeparatorLine}${SYMBOLS.SeparatorLine}${separatorLabel}${SYMBOLS.SeparatorLine}${SYMBOLS.SeparatorLine}`)}${EOL}`);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
const formattedChoice = this.#getFormattedChoice(choiceIndex);
|
|
921
|
+
const isChoiceActive = choiceIndex === this.activeIndex;
|
|
922
|
+
const isChoiceSelected = this.selectedIndexes.has(choiceIndex);
|
|
923
|
+
const isChoiceDisabled = this.#isChoiceDisabled(rawChoice);
|
|
924
|
+
const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex;
|
|
925
|
+
const showNextChoicesArrow = endIndex < this.filteredChoices.length && choiceIndex === endIndex - 1;
|
|
926
|
+
let prefixArrow = " ";
|
|
927
|
+
if (showPreviousChoicesArrow) prefixArrow = SYMBOLS.Previous + " ";
|
|
928
|
+
else if (showNextChoicesArrow) prefixArrow = SYMBOLS.Next + " ";
|
|
929
|
+
const prefix = `${prefixArrow}${isChoiceSelected ? SYMBOLS.Active : SYMBOLS.Inactive}`;
|
|
930
|
+
const formattedLabel = formattedChoice.label.padEnd(this.longestChoice < 10 ? this.longestChoice : 0);
|
|
931
|
+
const formattedDescription = formattedChoice.description ? ` - ${formattedChoice.description}` : "";
|
|
932
|
+
const disabledMessage = isChoiceDisabled && typeof rawChoice !== "string" && typeof rawChoice.disabled === "string" ? ` [${rawChoice.disabled}]` : "";
|
|
933
|
+
let textStyles;
|
|
934
|
+
if (isChoiceDisabled) textStyles = ["gray", "dim"];
|
|
935
|
+
else if (isChoiceActive) textStyles = ["white", "bold"];
|
|
936
|
+
else textStyles = ["gray"];
|
|
937
|
+
const str = `${prefix} ${styleText(textStyles, `${formattedLabel}${formattedDescription}${disabledMessage}`)}${EOL}`;
|
|
938
|
+
this.write(str);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async #handleReturn(resolve, render) {
|
|
942
|
+
this.#isValidating = true;
|
|
943
|
+
try {
|
|
944
|
+
const { values, labels } = this.#selectedChoices();
|
|
945
|
+
for (const validator of this.#validators) {
|
|
946
|
+
let validationResult;
|
|
947
|
+
const result = validator.validate(values);
|
|
948
|
+
if (result instanceof Promise) {
|
|
949
|
+
let dotCount = 1;
|
|
950
|
+
render({ validating: `validating${".".repeat(dotCount)}` });
|
|
951
|
+
const spinnerInterval = setInterval(() => {
|
|
952
|
+
dotCount = dotCount % 3 + 1;
|
|
953
|
+
render({ validating: `validating${".".repeat(dotCount)}` });
|
|
954
|
+
}, 300);
|
|
955
|
+
try {
|
|
956
|
+
validationResult = await result;
|
|
957
|
+
} finally {
|
|
958
|
+
clearInterval(spinnerInterval);
|
|
959
|
+
}
|
|
960
|
+
} else validationResult = result;
|
|
961
|
+
if (isValid(validationResult) === false) {
|
|
962
|
+
render({ error: resultError(validationResult) });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
render({ clearRender: true });
|
|
967
|
+
this.#showAnsweredQuestion(labels.join(", "));
|
|
968
|
+
this.write(SYMBOLS.ShowCursor);
|
|
969
|
+
this.destroy();
|
|
970
|
+
this.#onProcessExit();
|
|
971
|
+
process.off("exit", this.#boundExitEvent);
|
|
972
|
+
resolve(values);
|
|
973
|
+
} finally {
|
|
974
|
+
this.#isValidating = false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
#showAnsweredQuestion(choices, isAgentAnswer = false) {
|
|
978
|
+
const prefix = `${this.selectedIndexes.size === 0 && !isAgentAnswer ? SYMBOLS.Cross : SYMBOLS.Tick} ${styleText("bold", this.message)} ${SYMBOLS.Pointer}`;
|
|
979
|
+
const formattedChoice = styleText("yellow", choices);
|
|
980
|
+
this.write(`${prefix}${choices ? ` ${formattedChoice}` : ""}${EOL}`);
|
|
981
|
+
}
|
|
982
|
+
#selectedChoices() {
|
|
983
|
+
return [...this.selectedIndexes].reduce((acc, index) => {
|
|
984
|
+
const choice = this.filteredChoices[index];
|
|
985
|
+
if (typeof choice === "string") {
|
|
986
|
+
acc.values.push(choice);
|
|
987
|
+
acc.labels.push(choice);
|
|
988
|
+
} else if (!isSeparator(choice)) {
|
|
989
|
+
acc.values.push(choice.value);
|
|
990
|
+
acc.labels.push(choice.label);
|
|
991
|
+
}
|
|
992
|
+
return acc;
|
|
993
|
+
}, {
|
|
994
|
+
values: [],
|
|
995
|
+
labels: []
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
#onProcessExit() {
|
|
999
|
+
this.stdin.off("keypress", this.#boundKeyPressEvent);
|
|
1000
|
+
this.stdout.moveCursor(-this.stdout.columns, 0);
|
|
1001
|
+
this.stdout.clearScreenDown();
|
|
1002
|
+
this.write(SYMBOLS.ShowCursor);
|
|
1003
|
+
}
|
|
1004
|
+
#onKeypress(...args) {
|
|
1005
|
+
const [resolve, render, , key] = args;
|
|
1006
|
+
if (this.#isValidating) return;
|
|
1007
|
+
if (key.name === "up") {
|
|
1008
|
+
this.activeIndex = this.#findNextEnabledIndex(this.activeIndex, -1);
|
|
1009
|
+
render();
|
|
1010
|
+
} else if (key.name === "down") {
|
|
1011
|
+
this.activeIndex = this.#findNextEnabledIndex(this.activeIndex, 1);
|
|
1012
|
+
render();
|
|
1013
|
+
} else if (key.ctrl && key.name === "a") {
|
|
1014
|
+
const enabledIndexes = this.filteredChoices.flatMap((choice, index) => {
|
|
1015
|
+
if (isSeparator(choice)) return [];
|
|
1016
|
+
return this.#isChoiceDisabled(choice) ? [] : [index];
|
|
1017
|
+
});
|
|
1018
|
+
this.selectedIndexes = this.selectedIndexes.size === enabledIndexes.length ? /* @__PURE__ */ new Set() : new Set(enabledIndexes);
|
|
1019
|
+
render();
|
|
1020
|
+
} else if (key.name === "right") {
|
|
1021
|
+
const activeChoice = this.filteredChoices[this.activeIndex];
|
|
1022
|
+
if (!isSeparator(activeChoice) && !this.#isChoiceDisabled(activeChoice)) {
|
|
1023
|
+
this.selectedIndexes.add(this.activeIndex);
|
|
1024
|
+
render();
|
|
1025
|
+
}
|
|
1026
|
+
} else if (key.name === "left") {
|
|
1027
|
+
const activeChoice = this.filteredChoices[this.activeIndex];
|
|
1028
|
+
if (!isSeparator(activeChoice) && !this.#isChoiceDisabled(activeChoice)) {
|
|
1029
|
+
this.selectedIndexes = new Set([...this.selectedIndexes].filter((index) => index !== this.activeIndex));
|
|
1030
|
+
render();
|
|
1031
|
+
}
|
|
1032
|
+
} else if (key.name === "return") this.#handleReturn(resolve, render);
|
|
1033
|
+
else {
|
|
1034
|
+
if (!key.ctrl && this.options.autocomplete) {
|
|
1035
|
+
this.selectedIndexes.clear();
|
|
1036
|
+
this.activeIndex = this.#findFirstEnabledIndex();
|
|
1037
|
+
if (key.name === "backspace" && this.autocompleteValue.length > 0) this.autocompleteValue = this.autocompleteValue.slice(0, -1);
|
|
1038
|
+
else if (key.name !== "backspace") this.autocompleteValue += key.sequence;
|
|
1039
|
+
}
|
|
1040
|
+
render();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async listen() {
|
|
1044
|
+
if (this.skip) {
|
|
1045
|
+
this.destroy();
|
|
1046
|
+
const { values } = this.#selectedChoices();
|
|
1047
|
+
return values;
|
|
1048
|
+
}
|
|
1049
|
+
const answer = this.agent.nextAnswers.shift();
|
|
1050
|
+
if (answer !== void 0) {
|
|
1051
|
+
const formatedAnser = Array.isArray(answer) ? answer.join(", ") : answer;
|
|
1052
|
+
this.#showAnsweredQuestion(formatedAnser, true);
|
|
1053
|
+
this.destroy();
|
|
1054
|
+
return Array.isArray(answer) ? answer : [answer];
|
|
1055
|
+
}
|
|
1056
|
+
this.transformer = () => null;
|
|
1057
|
+
this.write(SYMBOLS.HideCursor);
|
|
1058
|
+
this.#showQuestion();
|
|
1059
|
+
const render = (options = {}) => {
|
|
1060
|
+
const { initialRender = false, clearRender = false, error = null, validating = null } = options;
|
|
1061
|
+
if (!initialRender) {
|
|
1062
|
+
let linesToClear = this.lastRender.endIndex - this.lastRender.startIndex;
|
|
1063
|
+
while (linesToClear > 0) {
|
|
1064
|
+
this.clearLastLine();
|
|
1065
|
+
linesToClear--;
|
|
1066
|
+
}
|
|
1067
|
+
if (this.options.autocomplete) {
|
|
1068
|
+
let linesToClear = Math.ceil(stringLength(`${SYMBOLS.Pointer} ${this.autocompleteValue}`) / this.stdout.columns);
|
|
1069
|
+
while (linesToClear > 0) {
|
|
1070
|
+
this.clearLastLine();
|
|
1071
|
+
linesToClear--;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (clearRender) {
|
|
1076
|
+
this.clearLastLine();
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (error || validating) {
|
|
1080
|
+
this.clearLastLine();
|
|
1081
|
+
this.#showQuestion(error, validating);
|
|
1082
|
+
}
|
|
1083
|
+
this.#showChoices();
|
|
1084
|
+
};
|
|
1085
|
+
render({ initialRender: true });
|
|
1086
|
+
this.#boundExitEvent = this.#onProcessExit.bind(this);
|
|
1087
|
+
process.once("exit", this.#boundExitEvent);
|
|
1088
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
1089
|
+
this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve, render);
|
|
1090
|
+
this.stdin.on("keypress", this.#boundKeyPressEvent);
|
|
1091
|
+
return promise;
|
|
1092
|
+
}
|
|
1093
|
+
#showQuestion(error = null, validating = null) {
|
|
1094
|
+
let hint = this.#showHint ? styleText("gray", `(Press ${styleText("bold", "<Ctrl+A>")} to toggle all, ${styleText("bold", "<Left/Right>")} to toggle, ${styleText("bold", "<Return>")} to submit)`) : "";
|
|
1095
|
+
if (validating) hint += `${hint.length > 0 ? " " : ""}${styleText("yellow", `[${validating}]`)}`;
|
|
1096
|
+
else if (error) hint += `${hint.length > 0 ? " " : ""}${styleText(["red", "bold"], `[${error}]`)}`;
|
|
1097
|
+
this.questionMessage = `${SYMBOLS.QuestionMark} ${styleText("bold", this.message)}${hint.length > 0 ? ` ${hint}` : ""}`;
|
|
1098
|
+
this.write(`${this.questionMessage}${EOL}`);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
//#endregion
|
|
1102
|
+
//#region src/index.ts
|
|
1103
|
+
async function question(message, options = {}) {
|
|
1104
|
+
const prompt = new QuestionPrompt({
|
|
1105
|
+
...options,
|
|
1106
|
+
message
|
|
1107
|
+
});
|
|
1108
|
+
const onErrorSignal = new AbortController();
|
|
1109
|
+
const onError = once(prompt, "error", { signal: onErrorSignal.signal });
|
|
1110
|
+
const result = await Promise.race([prompt.listen(), onError]);
|
|
1111
|
+
if (isAbortError(result)) {
|
|
1112
|
+
prompt.destroy();
|
|
1113
|
+
throw result[0];
|
|
1114
|
+
}
|
|
1115
|
+
onErrorSignal.abort();
|
|
1116
|
+
return result;
|
|
1117
|
+
}
|
|
1118
|
+
async function select(message, options) {
|
|
1119
|
+
const prompt = new SelectPrompt({
|
|
1120
|
+
...options,
|
|
1121
|
+
message
|
|
1122
|
+
});
|
|
1123
|
+
const onErrorSignal = new AbortController();
|
|
1124
|
+
const onError = once(prompt, "error", { signal: onErrorSignal.signal });
|
|
1125
|
+
const result = await Promise.race([prompt.listen(), onError]);
|
|
1126
|
+
if (isAbortError(result)) {
|
|
1127
|
+
prompt.destroy();
|
|
1128
|
+
throw result[0];
|
|
1129
|
+
}
|
|
1130
|
+
onErrorSignal.abort();
|
|
1131
|
+
return result;
|
|
1132
|
+
}
|
|
1133
|
+
async function confirm(message, options = {}) {
|
|
1134
|
+
const prompt = new ConfirmPrompt({
|
|
1135
|
+
...options,
|
|
1136
|
+
message
|
|
1137
|
+
});
|
|
1138
|
+
const onErrorSignal = new AbortController();
|
|
1139
|
+
const onError = once(prompt, "error", { signal: onErrorSignal.signal });
|
|
1140
|
+
const result = await Promise.race([prompt.listen(), onError]);
|
|
1141
|
+
if (isAbortError(result)) {
|
|
1142
|
+
prompt.destroy();
|
|
1143
|
+
throw result[0];
|
|
1144
|
+
}
|
|
1145
|
+
onErrorSignal.abort();
|
|
1146
|
+
return result;
|
|
1147
|
+
}
|
|
1148
|
+
async function multiselect(message, options) {
|
|
1149
|
+
const prompt = new MultiselectPrompt({
|
|
1150
|
+
...options,
|
|
1151
|
+
message
|
|
1152
|
+
});
|
|
1153
|
+
const onErrorSignal = new AbortController();
|
|
1154
|
+
const onError = once(prompt, "error", { signal: onErrorSignal.signal });
|
|
1155
|
+
const result = await Promise.race([prompt.listen(), onError]);
|
|
1156
|
+
if (isAbortError(result)) {
|
|
1157
|
+
prompt.destroy();
|
|
1158
|
+
throw result[0];
|
|
1159
|
+
}
|
|
1160
|
+
onErrorSignal.abort();
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
function isAbortError(error) {
|
|
1164
|
+
return Array.isArray(error) && error.length > 0 && error[0] instanceof Error;
|
|
1165
|
+
}
|
|
1166
|
+
const validators = { required };
|
|
1167
|
+
const transformers = {
|
|
1168
|
+
number,
|
|
1169
|
+
integer,
|
|
1170
|
+
url
|
|
1171
|
+
};
|
|
1172
|
+
//#endregion
|
|
1173
|
+
export { PromptAgent, confirm, multiselect, question, select, transformers, validators };
|
|
1174
|
+
|
|
1175
|
+
//# sourceMappingURL=index.mjs.map
|