@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.
Files changed (72) hide show
  1. package/lib/esm/PromptRunner.js +29 -0
  2. package/lib/esm/errors/PromptCancelledError.js +6 -0
  3. package/lib/esm/fn/autocomplete.js +60 -0
  4. package/lib/esm/fn/checkbox.js +28 -0
  5. package/lib/esm/fn/confirm.js +10 -0
  6. package/lib/esm/fn/index.js +6 -0
  7. package/lib/esm/fn/input.js +8 -0
  8. package/lib/esm/fn/list.js +18 -0
  9. package/lib/esm/fn/password.js +8 -0
  10. package/lib/esm/index.js +3 -0
  11. package/lib/esm/interfaces/NormalizedPromptQuestion.js +1 -0
  12. package/lib/esm/interfaces/PromptQuestion.js +1 -0
  13. package/lib/esm/utils/applyTransforms.js +10 -0
  14. package/lib/esm/utils/ensureNotCancelled.js +9 -0
  15. package/lib/esm/utils/getValidationError.js +14 -0
  16. package/lib/esm/utils/normalizeChoices.js +17 -0
  17. package/lib/esm/utils/normalizeQuestion.js +23 -0
  18. package/lib/esm/utils/processPrompt.js +20 -0
  19. package/lib/esm/utils/resolveListDefault.js +13 -0
  20. package/lib/esm/utils/resolveMaybe.js +6 -0
  21. package/lib/esm/utils/shouldAsk.js +9 -0
  22. package/lib/tsconfig.esm.tsbuildinfo +1 -0
  23. package/lib/types/PromptRunner.d.ts +6 -0
  24. package/lib/types/errors/PromptCancelledError.d.ts +3 -0
  25. package/lib/types/fn/autocomplete.d.ts +2 -0
  26. package/lib/types/fn/checkbox.d.ts +2 -0
  27. package/lib/types/fn/confirm.d.ts +2 -0
  28. package/lib/types/fn/index.d.ts +6 -0
  29. package/lib/types/fn/input.d.ts +2 -0
  30. package/lib/types/fn/list.d.ts +2 -0
  31. package/lib/types/fn/password.d.ts +2 -0
  32. package/lib/types/index.d.ts +3 -0
  33. package/lib/types/interfaces/NormalizedPromptQuestion.d.ts +9 -0
  34. package/lib/types/interfaces/PromptQuestion.d.ts +154 -0
  35. package/lib/types/utils/applyTransforms.d.ts +2 -0
  36. package/lib/types/utils/ensureNotCancelled.d.ts +1 -0
  37. package/lib/types/utils/getValidationError.d.ts +2 -0
  38. package/lib/types/utils/normalizeChoices.d.ts +8 -0
  39. package/lib/types/utils/normalizeQuestion.d.ts +3 -0
  40. package/lib/types/utils/processPrompt.d.ts +5 -0
  41. package/lib/types/utils/resolveListDefault.d.ts +3 -0
  42. package/lib/types/utils/resolveMaybe.d.ts +1 -0
  43. package/lib/types/utils/shouldAsk.d.ts +2 -0
  44. package/package.json +47 -0
  45. package/readme.md +112 -0
  46. package/src/PromptRunner.spec.ts +64 -0
  47. package/src/PromptRunner.ts +42 -0
  48. package/src/errors/PromptCancelledError.spec.ts +13 -0
  49. package/src/errors/PromptCancelledError.ts +6 -0
  50. package/src/fn/autocomplete.ts +77 -0
  51. package/src/fn/checkbox.ts +40 -0
  52. package/src/fn/confirm.ts +16 -0
  53. package/src/fn/index.ts +6 -0
  54. package/src/fn/input.ts +13 -0
  55. package/src/fn/list.ts +24 -0
  56. package/src/fn/password.ts +13 -0
  57. package/src/fn/prompts.spec.ts +175 -0
  58. package/src/index.ts +3 -0
  59. package/src/interfaces/NormalizedPromptQuestion.ts +10 -0
  60. package/src/interfaces/PromptQuestion.ts +172 -0
  61. package/src/utils/applyTransforms.ts +15 -0
  62. package/src/utils/ensureNotCancelled.ts +12 -0
  63. package/src/utils/getValidationError.ts +21 -0
  64. package/src/utils/normalizeChoices.ts +28 -0
  65. package/src/utils/normalizeQuestion.ts +31 -0
  66. package/src/utils/processPrompt.ts +29 -0
  67. package/src/utils/resolveListDefault.ts +22 -0
  68. package/src/utils/resolveMaybe.ts +10 -0
  69. package/src/utils/shouldAsk.ts +13 -0
  70. package/src/utils/utils.spec.ts +169 -0
  71. package/tsconfig.esm.json +27 -0
  72. 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,6 @@
1
+ export class PromptCancelledError extends Error {
2
+ constructor(message = "Prompt cancelled") {
3
+ super(message);
4
+ this.name = "PromptCancelledError";
5
+ }
6
+ }
@@ -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,6 @@
1
+ export { autocomplete } from "./autocomplete.js";
2
+ export { checkbox } from "./checkbox.js";
3
+ export { confirm } from "./confirm.js";
4
+ export { input } from "./input.js";
5
+ export { list } from "./list.js";
6
+ export { password } from "./password.js";
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./errors/PromptCancelledError.js";
2
+ export * from "./interfaces/PromptQuestion.js";
3
+ export * from "./PromptRunner.js";
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export function resolveMaybe(value, answers) {
2
+ if (typeof value === "function") {
3
+ return value(answers);
4
+ }
5
+ return value;
6
+ }
@@ -0,0 +1,9 @@
1
+ export async function shouldAsk(question, answers) {
2
+ if (question.when === undefined) {
3
+ return true;
4
+ }
5
+ if (typeof question.when === "function") {
6
+ return !!(await question.when(answers));
7
+ }
8
+ return question.when;
9
+ }