@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,154 @@
1
+ type MaybePromise<T> = T | Promise<T>;
2
+ /**
3
+ * Enumerates the built-in prompt types supported by the Ts.ED CLI.
4
+ */
5
+ export type PromptType = "input" | "password" | "confirm" | "list" | "checkbox" | "autocomplete";
6
+ /**
7
+ * Represents a single choice entry usable by select, checkbox, and autocomplete prompts.
8
+ */
9
+ export interface PromptChoice<Value = any> {
10
+ /**
11
+ * Human friendly label displayed in the prompt list.
12
+ */
13
+ name?: string;
14
+ /**
15
+ * Raw value returned when the choice is selected.
16
+ */
17
+ value?: Value;
18
+ /**
19
+ * Optional short label shown beside the main choice name.
20
+ */
21
+ short?: string;
22
+ /**
23
+ * Marks the choice as disabled (boolean or reason string).
24
+ */
25
+ disabled?: boolean | string;
26
+ /**
27
+ * For checkbox prompts, marks the choice as checked by default.
28
+ */
29
+ checked?: boolean;
30
+ }
31
+ /**
32
+ * Choice definition accepted by prompts. A plain value will be coerced to a `PromptChoice`.
33
+ */
34
+ export type PromptChoiceInput<Value = any> = PromptChoice<Value> | Value;
35
+ /**
36
+ * Transforms user input before it becomes part of the command context.
37
+ */
38
+ export type PromptTransformer = (input: any, answers: Record<string, any>, flags?: {
39
+ isFinal?: boolean;
40
+ }) => any;
41
+ /**
42
+ * Validates the user input, returning `true`, `false`, or an error string.
43
+ */
44
+ export type PromptValidator = (input: any, answers: Record<string, any>) => MaybePromise<boolean | string>;
45
+ /**
46
+ * Filters the answer into a different representation before persistence.
47
+ */
48
+ export type PromptFilter = (input: any, answers: Record<string, any>) => MaybePromise<any>;
49
+ /**
50
+ * Determines whether a prompt should run.
51
+ */
52
+ export type PromptWhen = boolean | ((answers: Record<string, any>) => MaybePromise<boolean>);
53
+ /**
54
+ * Base contract shared by every question type.
55
+ */
56
+ export interface PromptBaseQuestion<Value = any> {
57
+ /**
58
+ * Prompt type to render.
59
+ */
60
+ type: PromptType;
61
+ /**
62
+ * Unique answer key assigned to the prompt.
63
+ */
64
+ name: string;
65
+ /**
66
+ * Prompt label. Accepts a string or function (resolved at runtime).
67
+ */
68
+ message: string | ((answers: Record<string, any>) => MaybePromise<string>);
69
+ /**
70
+ * Allows skipping the prompt based on previous answers.
71
+ */
72
+ when?: PromptWhen;
73
+ /**
74
+ * Default input value or factory function returning one.
75
+ */
76
+ default?: Value | ((answers: Record<string, any>) => MaybePromise<Value>);
77
+ /**
78
+ * Mutates the visual input while the user types.
79
+ */
80
+ transformer?: PromptTransformer;
81
+ /**
82
+ * Validates user input. Return `false`/string to display an error.
83
+ */
84
+ validate?: PromptValidator;
85
+ /**
86
+ * Mutates the stored answer after validation.
87
+ */
88
+ filter?: PromptFilter;
89
+ /**
90
+ * Optional max number of rows visible in select/checkbox prompts.
91
+ */
92
+ pageSize?: number;
93
+ /**
94
+ * Whether select prompts loop when reaching boundaries.
95
+ */
96
+ loop?: boolean;
97
+ }
98
+ /**
99
+ * Plain text prompt.
100
+ */
101
+ export interface PromptInputQuestion extends PromptBaseQuestion<string> {
102
+ type: "input";
103
+ }
104
+ /**
105
+ * Hidden text prompt (e.g., passwords or tokens).
106
+ */
107
+ export interface PromptPasswordQuestion extends PromptBaseQuestion<string> {
108
+ type: "password";
109
+ /**
110
+ * Character used to mask input (default: •). Set `false` to show raw input.
111
+ */
112
+ mask?: string | boolean;
113
+ }
114
+ /**
115
+ * Boolean confirmation prompt (yes/no).
116
+ */
117
+ export interface PromptConfirmQuestion extends PromptBaseQuestion<boolean> {
118
+ type: "confirm";
119
+ }
120
+ /**
121
+ * Single-select prompt with predefined choices.
122
+ */
123
+ export interface PromptListQuestion extends PromptBaseQuestion<any> {
124
+ type: "list";
125
+ /**
126
+ * Available choices displayed to the user.
127
+ */
128
+ choices: PromptChoiceInput[];
129
+ }
130
+ /**
131
+ * Multi-select prompt where the result is an array of chosen values.
132
+ */
133
+ export interface PromptCheckboxQuestion extends PromptBaseQuestion<any[]> {
134
+ type: "checkbox";
135
+ /**
136
+ * Available choices displayed to the user.
137
+ */
138
+ choices: PromptChoiceInput[];
139
+ }
140
+ /**
141
+ * Searchable prompt that fetches choices dynamically.
142
+ */
143
+ export interface PromptAutocompleteQuestion extends PromptBaseQuestion<any> {
144
+ type: "autocomplete";
145
+ /**
146
+ * Async loader returning the set of choices filtered by the keyword.
147
+ */
148
+ source: (answers: Record<string, any>, keyword?: string) => MaybePromise<PromptChoiceInput[]>;
149
+ }
150
+ /**
151
+ * Union describing every supported Ts.ED CLI question type.
152
+ */
153
+ export type PromptQuestion = PromptInputQuestion | PromptPasswordQuestion | PromptConfirmQuestion | PromptListQuestion | PromptCheckboxQuestion | PromptAutocompleteQuestion;
154
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { PromptQuestion } from "../interfaces/PromptQuestion.js";
2
+ export declare function applyTransforms(question: PromptQuestion, answers: Record<string, any>, value: unknown): Promise<unknown>;
@@ -0,0 +1 @@
1
+ export declare function ensureNotCancelled<T>(value: T | symbol): T;
@@ -0,0 +1,2 @@
1
+ import type { PromptQuestion } from "../interfaces/PromptQuestion.js";
2
+ export declare function getValidationError(question: PromptQuestion, answers: Record<string, unknown>, value: unknown): Promise<string | undefined>;
@@ -0,0 +1,8 @@
1
+ import type { PromptChoiceInput } from "../interfaces/PromptQuestion.js";
2
+ export type NormalizedChoice = {
3
+ label: string;
4
+ value: any;
5
+ hint?: string;
6
+ checked?: boolean;
7
+ };
8
+ export declare function normalizeChoices(inputs?: PromptChoiceInput[]): NormalizedChoice[];
@@ -0,0 +1,3 @@
1
+ import type { NormalizedPromptQuestion } from "../interfaces/NormalizedPromptQuestion.js";
2
+ import type { PromptQuestion } from "../interfaces/PromptQuestion.js";
3
+ export declare function normalizeQuestion(question: PromptQuestion, answers: Record<string, unknown>): Promise<NormalizedPromptQuestion>;
@@ -0,0 +1,5 @@
1
+ import type { PromptQuestion } from "../interfaces/PromptQuestion.js";
2
+ export declare const CONTINUE: unique symbol;
3
+ type PromptExecutor = () => Promise<unknown> | unknown;
4
+ export declare function processPrompt(question: PromptQuestion, answers: Record<string, unknown>, cb: PromptExecutor): Promise<unknown>;
5
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { PromptAutocompleteQuestion, PromptListQuestion } from "../interfaces/PromptQuestion.js";
2
+ import type { NormalizedChoice } from "./normalizeChoices.js";
3
+ export declare function resolveListDefault(question: Pick<PromptListQuestion | PromptAutocompleteQuestion, "default">, choices: NormalizedChoice[]): any;
@@ -0,0 +1 @@
1
+ export declare function resolveMaybe<T>(value: T | ((answers: Record<string, any>) => T | Promise<T>) | undefined, answers: Record<string, any>): Promise<T> | T;
@@ -0,0 +1,2 @@
1
+ import type { PromptQuestion } from "../interfaces/PromptQuestion.js";
2
+ export declare function shouldAsk(question: PromptQuestion, answers: Record<string, any>): Promise<boolean>;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@tsed/cli-prompts",
3
+ "description": "Prompt runner and schema shared across Ts.ED CLIs",
4
+ "version": "7.0.0-beta.11",
5
+ "type": "module",
6
+ "main": "./lib/esm/index.js",
7
+ "source": "./src/index.ts",
8
+ "module": "./lib/esm/index.js",
9
+ "typings": "./lib/types/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "cli-tsed-source": "./src/index.ts",
13
+ "types": "./lib/types/index.d.ts",
14
+ "import": "./lib/esm/index.js",
15
+ "default": "./lib/esm/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "yarn build:ts",
20
+ "build:ts": "tsc --build tsconfig.esm.json",
21
+ "test": "vitest run",
22
+ "test:ci": "vitest run --coverage.thresholds.autoUpdate=true"
23
+ },
24
+ "dependencies": {
25
+ "@clack/prompts": "^0.7.0",
26
+ "@tsed/core": ">=8.21.0",
27
+ "@tsed/di": ">=8.21.0",
28
+ "@tsed/hooks": ">=8.21.0",
29
+ "@tsed/logger": ">=8.0.3"
30
+ },
31
+ "devDependencies": {
32
+ "@tsed/typescript": "7.0.0-beta.11",
33
+ "typescript": "5.6.2",
34
+ "vitest": "3.2.4"
35
+ },
36
+ "peerDependencies": {},
37
+ "repository": "https://github.com/tsedio/tsed-cli",
38
+ "bugs": {
39
+ "url": "https://github.com/tsedio/tsed-cli/issues"
40
+ },
41
+ "homepage": "https://github.com/tsedio/tsed-cli/tree/master/packages/cli-prompts",
42
+ "author": "Romain Lenzotti",
43
+ "license": "MIT",
44
+ "publishConfig": {
45
+ "tag": "beta"
46
+ }
47
+ }
package/readme.md ADDED
@@ -0,0 +1,112 @@
1
+ # @tsed/cli-prompts
2
+
3
+ <p style="text-align: center" align="center">
4
+ <a href="https://tsed.dev" target="_blank"><img src="https://tsed.dev/tsed-og.png" width="200" alt="Ts.ED logo"/></a>
5
+ </p>
6
+
7
+ [![Build & Release](https://github.com/tsedio/tsed-cli/workflows/Build%20&%20Release/badge.svg?branch=master)](https://github.com/tsedio/tsed-cli/actions?query=workflow%3A%22Build+%26+Release%22)
8
+ [![TypeScript](https://badges.frapsoft.com/typescript/love/typescript.svg?v=100)](https://github.com/ellerbrock/typescript-badges/)
9
+
10
+ > Ts.ED’s prompt runner, powered by `@clack/prompts`, with a declarative schema shared across every CLI package.
11
+
12
+ ## Goals
13
+
14
+ `@tsed/cli-prompts` extracts the reusable prompt subsystem that used to live in `@tsed/cli-core`. The package owns:
15
+
16
+ - The canonical `PromptQuestion` schema used by command providers and template generators.
17
+ - A `PromptRunner` service that evaluates `when` guards, resolves defaults, and dispatches to prompt handlers.
18
+ - A Clack-based implementation of every prompt type Ts.ED commands rely on (`input`, `password`, `confirm`, `list`, `checkbox`, `autocomplete`).
19
+ - Consistent cancellation/Error handling via `PromptCancelledError`.
20
+
21
+ Shipping this as a standalone package lets other workspaces (and downstream CLIs) reuse the exact same behavior without duplicating abstractions or third-party dependencies.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @tsed/cli-prompts @tsed/core @tsed/di
27
+ ```
28
+
29
+ > `@tsed/di` is required because the prompt runner is an injectable service.
30
+
31
+ ## Features
32
+
33
+ - **Clack UX**: Interactively renders all prompts via `@clack/prompts` (selects, multiselects, autocomplete loops, password masking, etc.).
34
+ - **Declarative schema**: Define questions with the standard Ts.ED structure (`name`, `type`, `message`, `choices`, `default`, `when`, `validate`, `filter`, `transformer`).
35
+ - **Async autocomplete**: Built-in support for searchable lists using the `source(answers, keyword)` contract used by `tsed add`, `tsed generate`, and MCP tooling.
36
+ - **Cancellation safety**: `ensureNotCancelled()` converts Ctrl+C / ESC exits into a thrown `PromptCancelledError`, allowing CLI hosts to render a friendly message and abort.
37
+ - **Transformer & validation helpers**: All handlers consistently run `transformer`, `filter`, and `validate` hooks before finalizing the answer.
38
+
39
+ ## Getting started
40
+
41
+ When you build a CLI that uses `@tsed/cli-core`, the prompt runner is already injected for you. To use it directly, register it inside your DI context:
42
+
43
+ ```typescript
44
+ import {CliPlatformTest} from "@tsed/cli-testing";
45
+ import {PromptRunner, PromptQuestion} from "@tsed/cli-prompts";
46
+
47
+ await CliPlatformTest.create();
48
+ const runner = await CliPlatformTest.invoke<PromptRunner>(PromptRunner);
49
+
50
+ const questions: PromptQuestion[] = [
51
+ {
52
+ type: "input",
53
+ name: "packageName",
54
+ message: "Package name",
55
+ validate(value) {
56
+ return value ? true : "Package name is required";
57
+ }
58
+ },
59
+ {
60
+ type: "autocomplete",
61
+ name: "feature",
62
+ message: "Choose a feature",
63
+ source: async (_answers, keyword = "") => {
64
+ return ["cli-core", "cli-prompts", "cli-mcp"].filter((entry) => entry.includes(keyword)).map((value) => ({name: value, value}));
65
+ }
66
+ }
67
+ ];
68
+
69
+ const answers = await runner.run(questions);
70
+ console.log(answers);
71
+ ```
72
+
73
+ ### Supported question types
74
+
75
+ | Type | Description |
76
+ | -------------- | -------------------------------------------------------- |
77
+ | `input` | Free-form text entry with optional transform/validation. |
78
+ | `password` | Hidden input with customizable mask characters. |
79
+ | `confirm` | Boolean yes/no selection. |
80
+ | `list` | Single-select list rendered with Clack’s `select`. |
81
+ | `checkbox` | Multi-select list rendered with `multiselect`. |
82
+ | `autocomplete` | Searchable list that re-queries a `source` function. |
83
+
84
+ Every question can declare `when` guards, `default` values (including functions that read previous answers), `pageSize`, and `choices` metadata (`name`, `value`, `short`, `checked`, `disabled`).
85
+
86
+ ### Handling cancellations
87
+
88
+ Ctrl+C and ESC bubble up as `PromptCancelledError`. Catch it at the application boundary (as `CliCore` does) if you need custom messaging:
89
+
90
+ ```typescript
91
+ import {PromptRunner, PromptCancelledError} from "@tsed/cli-prompts";
92
+
93
+ try {
94
+ await runner.run(questionList);
95
+ } catch (er) {
96
+ if (er instanceof PromptCancelledError) {
97
+ console.log("Prompt cancelled by the user.");
98
+ process.exit(0);
99
+ }
100
+ throw er;
101
+ }
102
+ ```
103
+
104
+ ## Development
105
+
106
+ This package follows the same conventions as other Ts.ED workspaces:
107
+
108
+ - `yarn workspace @tsed/cli-prompts build:ts` – compile to `lib/esm` + `lib/types`.
109
+ - `yarn workspace @tsed/cli-prompts test` – run the Vitest suite.
110
+ - `yarn lint` – run the monorepo ESLint config (covers this workspace automatically).
111
+
112
+ Please read the [contributing guide](https://tsed.dev/CONTRIBUTING.html) before submitting changes.
@@ -0,0 +1,64 @@
1
+ import {beforeEach, describe, expect, it, vi} from "vitest";
2
+
3
+ import {PromptRunner} from "./PromptRunner.js";
4
+
5
+ const handlerMap = vi.hoisted(() => ({
6
+ input: vi.fn()
7
+ }));
8
+
9
+ const handlerModule = vi.hoisted(() => {
10
+ return new Proxy(handlerMap, {
11
+ get(target, prop) {
12
+ return Reflect.get(target, prop);
13
+ }
14
+ });
15
+ });
16
+
17
+ vi.mock("./fn/index.js", () => handlerModule);
18
+
19
+ describe("PromptRunner", () => {
20
+ beforeEach(() => {
21
+ handlerMap.input.mockReset();
22
+ });
23
+
24
+ it("should execute supported prompt types and merge answers", async () => {
25
+ handlerMap.input.mockResolvedValueOnce("fooValue").mockResolvedValueOnce("barValue");
26
+
27
+ const runner = new PromptRunner();
28
+ const answers = await runner.run(
29
+ [
30
+ {type: "input", name: "foo", message: "Foo"},
31
+ false as any,
32
+ {
33
+ type: "input",
34
+ name: "bar",
35
+ message: () => "Bar",
36
+ when: (current) => current.foo === "fooValue"
37
+ }
38
+ ],
39
+ {initial: true}
40
+ );
41
+
42
+ expect(handlerMap.input).toHaveBeenNthCalledWith(
43
+ 1,
44
+ expect.objectContaining({name: "foo", message: "Foo"}),
45
+ expect.objectContaining({initial: true})
46
+ );
47
+ expect(handlerMap.input).toHaveBeenNthCalledWith(
48
+ 2,
49
+ expect.objectContaining({name: "bar", message: "Bar"}),
50
+ expect.objectContaining({foo: "fooValue", initial: true})
51
+ );
52
+ expect(answers).toEqual({
53
+ foo: "fooValue",
54
+ bar: "barValue"
55
+ });
56
+ });
57
+
58
+ it("should throw when prompt type is unsupported", async () => {
59
+ const runner = new PromptRunner();
60
+ (handlerMap as any).unknown = undefined;
61
+
62
+ await expect(runner.run([{type: "unknown" as any, name: "noop", message: "Noop"}])).rejects.toThrow("Unsupported prompt type: unknown");
63
+ });
64
+ });
@@ -0,0 +1,42 @@
1
+ import {injectable} from "@tsed/di";
2
+
3
+ import * as fn from "./fn/index.js";
4
+ import type {NormalizedPromptQuestion} from "./interfaces/NormalizedPromptQuestion.js";
5
+ import type {PromptQuestion} from "./interfaces/PromptQuestion.js";
6
+ import {normalizeQuestion} from "./utils/normalizeQuestion.js";
7
+ import {shouldAsk} from "./utils/shouldAsk.js";
8
+
9
+ export class PromptRunner {
10
+ async run(questions: PromptQuestion[] | undefined, initialAnswers: Record<string, any> = {}) {
11
+ const queue = ([] as PromptQuestion[]).concat(questions ?? []).filter(Boolean);
12
+
13
+ const answers = {...initialAnswers};
14
+ const collected: Record<string, any> = {};
15
+
16
+ for (const question of queue) {
17
+ if (!(await shouldAsk(question, answers))) {
18
+ continue;
19
+ }
20
+
21
+ const normalized = await normalizeQuestion(question, answers);
22
+ const response = await this.prompt(normalized, answers);
23
+
24
+ answers[question.name] = response;
25
+ collected[question.name] = response;
26
+ }
27
+
28
+ return collected;
29
+ }
30
+
31
+ protected async prompt(question: NormalizedPromptQuestion, answers: Record<string, unknown>): Promise<any> {
32
+ const type = question.type;
33
+
34
+ if (!fn[type]) {
35
+ throw new Error(`Unsupported prompt type: ${type as string}`);
36
+ }
37
+
38
+ return fn[type](question as never, answers);
39
+ }
40
+ }
41
+
42
+ injectable(PromptRunner);
@@ -0,0 +1,13 @@
1
+ import {describe, expect, it} from "vitest";
2
+
3
+ import {PromptCancelledError} from "./PromptCancelledError.js";
4
+
5
+ describe("PromptCancelledError", () => {
6
+ it("should set name and default message", () => {
7
+ const error = new PromptCancelledError();
8
+
9
+ expect(error).toBeInstanceOf(Error);
10
+ expect(error.name).toBe("PromptCancelledError");
11
+ expect(error.message).toBe("Prompt cancelled");
12
+ });
13
+ });
@@ -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,77 @@
1
+ import {select, text} from "@clack/prompts";
2
+
3
+ import type {NormalizedPromptQuestion} from "../interfaces/NormalizedPromptQuestion.js";
4
+ import type {PromptAutocompleteQuestion} from "../interfaces/PromptQuestion.js";
5
+ import {ensureNotCancelled} from "../utils/ensureNotCancelled.js";
6
+ import {normalizeChoices} from "../utils/normalizeChoices.js";
7
+ import {CONTINUE, processPrompt} from "../utils/processPrompt.js";
8
+ import {resolveListDefault} from "../utils/resolveListDefault.js";
9
+
10
+ const SEARCH_ACTION = "__tsed_cli_search_again__";
11
+
12
+ export async function autocomplete(question: NormalizedPromptQuestion, answers: Record<string, unknown>) {
13
+ if (!question.source) {
14
+ throw new Error(`Question "${question.name}" must provide a source for autocomplete prompts.`);
15
+ }
16
+
17
+ let keyword = "";
18
+
19
+ let choices = await resolveAutocompleteChoices(question.source, answers, keyword);
20
+
21
+ async function display() {
22
+ keyword = await promptKeyword(question.message, keyword, true);
23
+ choices = await resolveAutocompleteChoices(question.source!, answers, keyword);
24
+ }
25
+
26
+ return processPrompt(question, answers, async () => {
27
+ if (!choices.length) {
28
+ await display();
29
+ return CONTINUE;
30
+ }
31
+
32
+ const selection = await select({
33
+ message: buildAutocompleteMessage(question.message, keyword),
34
+ options: [
35
+ ...choices.map((choice) => ({
36
+ label: choice.label,
37
+ value: choice.value,
38
+ hint: choice.hint
39
+ })),
40
+ {
41
+ label: "🔍 Search again",
42
+ value: SEARCH_ACTION,
43
+ hint: "Type another keyword"
44
+ }
45
+ ],
46
+ initialValue: resolveListDefault(question, choices),
47
+ maxItems: question.pageSize
48
+ });
49
+
50
+ if (selection === SEARCH_ACTION) {
51
+ await display();
52
+ return CONTINUE;
53
+ }
54
+
55
+ return selection;
56
+ });
57
+ }
58
+
59
+ async function resolveAutocompleteChoices(source: PromptAutocompleteQuestion["source"], answers: Record<string, any>, keyword: string) {
60
+ const items = await source!(answers, keyword);
61
+
62
+ return normalizeChoices(items);
63
+ }
64
+
65
+ async function promptKeyword(message: string, keyword: string, emptyState: boolean) {
66
+ const label = emptyState ? `${message} (no matches, type to search)` : `${message} (type to refine search)`;
67
+ const result = await text({
68
+ message: label,
69
+ initialValue: keyword
70
+ });
71
+
72
+ return ensureNotCancelled(result).trim();
73
+ }
74
+
75
+ function buildAutocompleteMessage(message: string, keyword: string) {
76
+ return keyword ? `${message} (filter: ${keyword})` : message;
77
+ }
@@ -0,0 +1,40 @@
1
+ import {multiselect} from "@clack/prompts";
2
+ import {isArray} from "@tsed/core/utils/isArray";
3
+
4
+ import type {NormalizedPromptQuestion} from "../interfaces/NormalizedPromptQuestion.js";
5
+ import type {NormalizedChoice} from "../utils/normalizeChoices.js";
6
+ import {processPrompt} from "../utils/processPrompt.js";
7
+
8
+ export async function checkbox(question: NormalizedPromptQuestion, answers: Record<string, unknown>) {
9
+ if (!question.choices?.length) {
10
+ throw new Error(`Question "${question.name}" does not provide any choices`);
11
+ }
12
+
13
+ const initialValues = resolveCheckboxDefaults(question, question.choices);
14
+
15
+ return processPrompt(question, answers, () =>
16
+ multiselect({
17
+ message: question.message,
18
+ options: question.choices!.map((choice) => ({
19
+ label: choice.label,
20
+ value: choice.value,
21
+ hint: choice.hint
22
+ })),
23
+ initialValues
24
+ })
25
+ );
26
+ }
27
+
28
+ function resolveCheckboxDefaults(question: NormalizedPromptQuestion, choices: NormalizedChoice[]) {
29
+ if (isArray(question.default)) {
30
+ return question.default;
31
+ }
32
+
33
+ if (question.default !== undefined) {
34
+ return [question.default];
35
+ }
36
+
37
+ const checkedValues = choices.filter((choice) => choice.checked).map((choice) => choice.value);
38
+
39
+ return checkedValues.length ? checkedValues : [];
40
+ }
@@ -0,0 +1,16 @@
1
+ import {confirm as c} from "@clack/prompts";
2
+ import {isBoolean} from "@tsed/core/utils/isBoolean.js";
3
+
4
+ import type {NormalizedPromptQuestion} from "../interfaces/NormalizedPromptQuestion.js";
5
+ import {processPrompt} from "../utils/processPrompt.js";
6
+
7
+ export async function confirm(question: NormalizedPromptQuestion, answers: Record<string, unknown>) {
8
+ const initialValue = isBoolean(question.default) ? question.default : undefined;
9
+
10
+ return processPrompt(question, answers, () =>
11
+ c({
12
+ message: question.message,
13
+ initialValue
14
+ })
15
+ );
16
+ }
@@ -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,13 @@
1
+ import {text} from "@clack/prompts";
2
+
3
+ import type {NormalizedPromptQuestion} from "../interfaces/NormalizedPromptQuestion.js";
4
+ import {processPrompt} from "../utils/processPrompt.js";
5
+
6
+ export async function input(question: NormalizedPromptQuestion, answers: Record<string, unknown>) {
7
+ return processPrompt(question, answers, () =>
8
+ text({
9
+ message: question.message,
10
+ initialValue: String(question.default ?? "")
11
+ })
12
+ );
13
+ }
package/src/fn/list.ts ADDED
@@ -0,0 +1,24 @@
1
+ import {select} from "@clack/prompts";
2
+
3
+ import type {NormalizedPromptQuestion} from "../interfaces/NormalizedPromptQuestion.js";
4
+ import {processPrompt} from "../utils/processPrompt.js";
5
+ import {resolveListDefault} from "../utils/resolveListDefault.js";
6
+
7
+ export async function list(question: NormalizedPromptQuestion, answers: Record<string, unknown>) {
8
+ if (!question.choices?.length) {
9
+ throw new Error(`Question "${question.name}" does not provide any choices`);
10
+ }
11
+
12
+ return processPrompt(question, answers, () =>
13
+ select({
14
+ message: question.message,
15
+ options: question.choices!.map((choice) => ({
16
+ label: choice.label,
17
+ value: choice.value,
18
+ hint: choice.hint
19
+ })),
20
+ initialValue: resolveListDefault(question, question.choices!),
21
+ maxItems: question.pageSize
22
+ })
23
+ );
24
+ }