@tsed/cli-prompts 7.0.0-beta.11
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/lib/esm/PromptRunner.js +29 -0
- package/lib/esm/errors/PromptCancelledError.js +6 -0
- package/lib/esm/fn/autocomplete.js +60 -0
- package/lib/esm/fn/checkbox.js +28 -0
- package/lib/esm/fn/confirm.js +10 -0
- package/lib/esm/fn/index.js +6 -0
- package/lib/esm/fn/input.js +8 -0
- package/lib/esm/fn/list.js +18 -0
- package/lib/esm/fn/password.js +8 -0
- package/lib/esm/index.js +3 -0
- package/lib/esm/interfaces/NormalizedPromptQuestion.js +1 -0
- package/lib/esm/interfaces/PromptQuestion.js +1 -0
- package/lib/esm/utils/applyTransforms.js +10 -0
- package/lib/esm/utils/ensureNotCancelled.js +9 -0
- package/lib/esm/utils/getValidationError.js +14 -0
- package/lib/esm/utils/normalizeChoices.js +17 -0
- package/lib/esm/utils/normalizeQuestion.js +23 -0
- package/lib/esm/utils/processPrompt.js +20 -0
- package/lib/esm/utils/resolveListDefault.js +13 -0
- package/lib/esm/utils/resolveMaybe.js +6 -0
- package/lib/esm/utils/shouldAsk.js +9 -0
- package/lib/tsconfig.esm.tsbuildinfo +1 -0
- package/lib/types/PromptRunner.d.ts +6 -0
- package/lib/types/errors/PromptCancelledError.d.ts +3 -0
- package/lib/types/fn/autocomplete.d.ts +2 -0
- package/lib/types/fn/checkbox.d.ts +2 -0
- package/lib/types/fn/confirm.d.ts +2 -0
- package/lib/types/fn/index.d.ts +6 -0
- package/lib/types/fn/input.d.ts +2 -0
- package/lib/types/fn/list.d.ts +2 -0
- package/lib/types/fn/password.d.ts +2 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/interfaces/NormalizedPromptQuestion.d.ts +9 -0
- package/lib/types/interfaces/PromptQuestion.d.ts +154 -0
- package/lib/types/utils/applyTransforms.d.ts +2 -0
- package/lib/types/utils/ensureNotCancelled.d.ts +1 -0
- package/lib/types/utils/getValidationError.d.ts +2 -0
- package/lib/types/utils/normalizeChoices.d.ts +8 -0
- package/lib/types/utils/normalizeQuestion.d.ts +3 -0
- package/lib/types/utils/processPrompt.d.ts +5 -0
- package/lib/types/utils/resolveListDefault.d.ts +3 -0
- package/lib/types/utils/resolveMaybe.d.ts +1 -0
- package/lib/types/utils/shouldAsk.d.ts +2 -0
- package/package.json +47 -0
- package/readme.md +112 -0
- package/src/PromptRunner.spec.ts +64 -0
- package/src/PromptRunner.ts +42 -0
- package/src/errors/PromptCancelledError.spec.ts +13 -0
- package/src/errors/PromptCancelledError.ts +6 -0
- package/src/fn/autocomplete.ts +77 -0
- package/src/fn/checkbox.ts +40 -0
- package/src/fn/confirm.ts +16 -0
- package/src/fn/index.ts +6 -0
- package/src/fn/input.ts +13 -0
- package/src/fn/list.ts +24 -0
- package/src/fn/password.ts +13 -0
- package/src/fn/prompts.spec.ts +175 -0
- package/src/index.ts +3 -0
- package/src/interfaces/NormalizedPromptQuestion.ts +10 -0
- package/src/interfaces/PromptQuestion.ts +172 -0
- package/src/utils/applyTransforms.ts +15 -0
- package/src/utils/ensureNotCancelled.ts +12 -0
- package/src/utils/getValidationError.ts +21 -0
- package/src/utils/normalizeChoices.ts +28 -0
- package/src/utils/normalizeQuestion.ts +31 -0
- package/src/utils/processPrompt.ts +29 -0
- package/src/utils/resolveListDefault.ts +22 -0
- package/src/utils/resolveMaybe.ts +10 -0
- package/src/utils/shouldAsk.ts +13 -0
- package/src/utils/utils.spec.ts +169 -0
- package/tsconfig.esm.json +27 -0
- package/vitest.config.mts +22 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { injectable } from "@tsed/di";
|
|
2
|
+
import * as fn from "./fn/index.js";
|
|
3
|
+
import { normalizeQuestion } from "./utils/normalizeQuestion.js";
|
|
4
|
+
import { shouldAsk } from "./utils/shouldAsk.js";
|
|
5
|
+
export class PromptRunner {
|
|
6
|
+
async run(questions, initialAnswers = {}) {
|
|
7
|
+
const queue = [].concat(questions ?? []).filter(Boolean);
|
|
8
|
+
const answers = { ...initialAnswers };
|
|
9
|
+
const collected = {};
|
|
10
|
+
for (const question of queue) {
|
|
11
|
+
if (!(await shouldAsk(question, answers))) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const normalized = await normalizeQuestion(question, answers);
|
|
15
|
+
const response = await this.prompt(normalized, answers);
|
|
16
|
+
answers[question.name] = response;
|
|
17
|
+
collected[question.name] = response;
|
|
18
|
+
}
|
|
19
|
+
return collected;
|
|
20
|
+
}
|
|
21
|
+
async prompt(question, answers) {
|
|
22
|
+
const type = question.type;
|
|
23
|
+
if (!fn[type]) {
|
|
24
|
+
throw new Error(`Unsupported prompt type: ${type}`);
|
|
25
|
+
}
|
|
26
|
+
return fn[type](question, answers);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
injectable(PromptRunner);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { select, text } from "@clack/prompts";
|
|
2
|
+
import { ensureNotCancelled } from "../utils/ensureNotCancelled.js";
|
|
3
|
+
import { normalizeChoices } from "../utils/normalizeChoices.js";
|
|
4
|
+
import { CONTINUE, processPrompt } from "../utils/processPrompt.js";
|
|
5
|
+
import { resolveListDefault } from "../utils/resolveListDefault.js";
|
|
6
|
+
const SEARCH_ACTION = "__tsed_cli_search_again__";
|
|
7
|
+
export async function autocomplete(question, answers) {
|
|
8
|
+
if (!question.source) {
|
|
9
|
+
throw new Error(`Question "${question.name}" must provide a source for autocomplete prompts.`);
|
|
10
|
+
}
|
|
11
|
+
let keyword = "";
|
|
12
|
+
let choices = await resolveAutocompleteChoices(question.source, answers, keyword);
|
|
13
|
+
async function display() {
|
|
14
|
+
keyword = await promptKeyword(question.message, keyword, true);
|
|
15
|
+
choices = await resolveAutocompleteChoices(question.source, answers, keyword);
|
|
16
|
+
}
|
|
17
|
+
return processPrompt(question, answers, async () => {
|
|
18
|
+
if (!choices.length) {
|
|
19
|
+
await display();
|
|
20
|
+
return CONTINUE;
|
|
21
|
+
}
|
|
22
|
+
const selection = await select({
|
|
23
|
+
message: buildAutocompleteMessage(question.message, keyword),
|
|
24
|
+
options: [
|
|
25
|
+
...choices.map((choice) => ({
|
|
26
|
+
label: choice.label,
|
|
27
|
+
value: choice.value,
|
|
28
|
+
hint: choice.hint
|
|
29
|
+
})),
|
|
30
|
+
{
|
|
31
|
+
label: "🔍 Search again",
|
|
32
|
+
value: SEARCH_ACTION,
|
|
33
|
+
hint: "Type another keyword"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
initialValue: resolveListDefault(question, choices),
|
|
37
|
+
maxItems: question.pageSize
|
|
38
|
+
});
|
|
39
|
+
if (selection === SEARCH_ACTION) {
|
|
40
|
+
await display();
|
|
41
|
+
return CONTINUE;
|
|
42
|
+
}
|
|
43
|
+
return selection;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function resolveAutocompleteChoices(source, answers, keyword) {
|
|
47
|
+
const items = await source(answers, keyword);
|
|
48
|
+
return normalizeChoices(items);
|
|
49
|
+
}
|
|
50
|
+
async function promptKeyword(message, keyword, emptyState) {
|
|
51
|
+
const label = emptyState ? `${message} (no matches, type to search)` : `${message} (type to refine search)`;
|
|
52
|
+
const result = await text({
|
|
53
|
+
message: label,
|
|
54
|
+
initialValue: keyword
|
|
55
|
+
});
|
|
56
|
+
return ensureNotCancelled(result).trim();
|
|
57
|
+
}
|
|
58
|
+
function buildAutocompleteMessage(message, keyword) {
|
|
59
|
+
return keyword ? `${message} (filter: ${keyword})` : message;
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { multiselect } from "@clack/prompts";
|
|
2
|
+
import { isArray } from "@tsed/core/utils/isArray";
|
|
3
|
+
import { processPrompt } from "../utils/processPrompt.js";
|
|
4
|
+
export async function checkbox(question, answers) {
|
|
5
|
+
if (!question.choices?.length) {
|
|
6
|
+
throw new Error(`Question "${question.name}" does not provide any choices`);
|
|
7
|
+
}
|
|
8
|
+
const initialValues = resolveCheckboxDefaults(question, question.choices);
|
|
9
|
+
return processPrompt(question, answers, () => multiselect({
|
|
10
|
+
message: question.message,
|
|
11
|
+
options: question.choices.map((choice) => ({
|
|
12
|
+
label: choice.label,
|
|
13
|
+
value: choice.value,
|
|
14
|
+
hint: choice.hint
|
|
15
|
+
})),
|
|
16
|
+
initialValues
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
function resolveCheckboxDefaults(question, choices) {
|
|
20
|
+
if (isArray(question.default)) {
|
|
21
|
+
return question.default;
|
|
22
|
+
}
|
|
23
|
+
if (question.default !== undefined) {
|
|
24
|
+
return [question.default];
|
|
25
|
+
}
|
|
26
|
+
const checkedValues = choices.filter((choice) => choice.checked).map((choice) => choice.value);
|
|
27
|
+
return checkedValues.length ? checkedValues : [];
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { confirm as c } from "@clack/prompts";
|
|
2
|
+
import { isBoolean } from "@tsed/core/utils/isBoolean.js";
|
|
3
|
+
import { processPrompt } from "../utils/processPrompt.js";
|
|
4
|
+
export async function confirm(question, answers) {
|
|
5
|
+
const initialValue = isBoolean(question.default) ? question.default : undefined;
|
|
6
|
+
return processPrompt(question, answers, () => c({
|
|
7
|
+
message: question.message,
|
|
8
|
+
initialValue
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { text } from "@clack/prompts";
|
|
2
|
+
import { processPrompt } from "../utils/processPrompt.js";
|
|
3
|
+
export async function input(question, answers) {
|
|
4
|
+
return processPrompt(question, answers, () => text({
|
|
5
|
+
message: question.message,
|
|
6
|
+
initialValue: String(question.default ?? "")
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { select } from "@clack/prompts";
|
|
2
|
+
import { processPrompt } from "../utils/processPrompt.js";
|
|
3
|
+
import { resolveListDefault } from "../utils/resolveListDefault.js";
|
|
4
|
+
export async function list(question, answers) {
|
|
5
|
+
if (!question.choices?.length) {
|
|
6
|
+
throw new Error(`Question "${question.name}" does not provide any choices`);
|
|
7
|
+
}
|
|
8
|
+
return processPrompt(question, answers, () => select({
|
|
9
|
+
message: question.message,
|
|
10
|
+
options: question.choices.map((choice) => ({
|
|
11
|
+
label: choice.label,
|
|
12
|
+
value: choice.value,
|
|
13
|
+
hint: choice.hint
|
|
14
|
+
})),
|
|
15
|
+
initialValue: resolveListDefault(question, question.choices),
|
|
16
|
+
maxItems: question.pageSize
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { password as clackPassword } from "@clack/prompts";
|
|
2
|
+
import { processPrompt } from "../utils/processPrompt.js";
|
|
3
|
+
export async function password(question, answers) {
|
|
4
|
+
return processPrompt(question, answers, () => clackPassword({
|
|
5
|
+
message: question.message,
|
|
6
|
+
mask: question.mask === false ? undefined : String(question.mask || "•")
|
|
7
|
+
}));
|
|
8
|
+
}
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function applyTransforms(question, answers, value) {
|
|
2
|
+
let next = value;
|
|
3
|
+
if (question.transformer) {
|
|
4
|
+
next = await question.transformer(next, answers, { isFinal: true });
|
|
5
|
+
}
|
|
6
|
+
if (question.filter) {
|
|
7
|
+
next = await question.filter(next, answers);
|
|
8
|
+
}
|
|
9
|
+
return next;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cancel as cancelPrompt, isCancel } from "@clack/prompts";
|
|
2
|
+
import { PromptCancelledError } from "../errors/PromptCancelledError.js";
|
|
3
|
+
export function ensureNotCancelled(value) {
|
|
4
|
+
if (isCancel(value)) {
|
|
5
|
+
cancelPrompt();
|
|
6
|
+
throw new PromptCancelledError();
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isString } from "@tsed/core";
|
|
2
|
+
export async function getValidationError(question, answers, value) {
|
|
3
|
+
if (!question.validate) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const result = await question.validate(value, answers);
|
|
7
|
+
if (result === false) {
|
|
8
|
+
return "Invalid value.";
|
|
9
|
+
}
|
|
10
|
+
if (isString(result)) {
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function normalizeChoices(inputs = []) {
|
|
2
|
+
return inputs.map((choice) => {
|
|
3
|
+
if (typeof choice === "object" && choice !== null && "value" in choice) {
|
|
4
|
+
const resolvedValue = choice.value ?? choice.name;
|
|
5
|
+
return {
|
|
6
|
+
label: choice.name ?? String(resolvedValue ?? ""),
|
|
7
|
+
value: resolvedValue,
|
|
8
|
+
hint: choice.short,
|
|
9
|
+
checked: choice.checked
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
label: String(choice),
|
|
14
|
+
value: choice
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { normalizeChoices } from "./normalizeChoices.js";
|
|
2
|
+
import { resolveMaybe } from "./resolveMaybe.js";
|
|
3
|
+
export async function normalizeQuestion(question, answers) {
|
|
4
|
+
const normalized = {
|
|
5
|
+
...question,
|
|
6
|
+
name: question.name,
|
|
7
|
+
type: question.type,
|
|
8
|
+
message: await resolveMaybe(question.message, answers)
|
|
9
|
+
};
|
|
10
|
+
if ("default" in question && question.default !== undefined) {
|
|
11
|
+
normalized.default = await resolveMaybe(question.default, answers);
|
|
12
|
+
}
|
|
13
|
+
if ("choices" in question && question.choices?.length) {
|
|
14
|
+
normalized.choices = normalizeChoices([...question.choices]);
|
|
15
|
+
}
|
|
16
|
+
if ("source" in question && question.source) {
|
|
17
|
+
const source = question.source;
|
|
18
|
+
normalized.source = (state, keyword) => {
|
|
19
|
+
return source({ ...answers, ...state }, keyword);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { note } from "@clack/prompts";
|
|
2
|
+
import { applyTransforms } from "./applyTransforms.js";
|
|
3
|
+
import { ensureNotCancelled } from "./ensureNotCancelled.js";
|
|
4
|
+
import { getValidationError } from "./getValidationError.js";
|
|
5
|
+
export const CONTINUE = Symbol.for("prompt:continue");
|
|
6
|
+
export async function processPrompt(question, answers, cb) {
|
|
7
|
+
while (true) {
|
|
8
|
+
const result = await cb();
|
|
9
|
+
if (result === CONTINUE) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const value = ensureNotCancelled(result);
|
|
13
|
+
const transformed = await applyTransforms(question, answers, value);
|
|
14
|
+
const error = await getValidationError(question, answers, transformed);
|
|
15
|
+
if (!error) {
|
|
16
|
+
return transformed;
|
|
17
|
+
}
|
|
18
|
+
note(error, "Validation error");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function resolveListDefault(question, choices) {
|
|
2
|
+
if (question.default !== undefined) {
|
|
3
|
+
if (typeof question.default === "number") {
|
|
4
|
+
return choices[question.default]?.value ?? choices[0]?.value;
|
|
5
|
+
}
|
|
6
|
+
return question.default;
|
|
7
|
+
}
|
|
8
|
+
const checked = choices.find((choice) => choice.checked);
|
|
9
|
+
if (checked) {
|
|
10
|
+
return checked.value;
|
|
11
|
+
}
|
|
12
|
+
return choices[0]?.value;
|
|
13
|
+
}
|