@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,13 @@
|
|
|
1
|
+
import {password as clackPassword} from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
import type {NormalizedPromptQuestion} from "../interfaces/NormalizedPromptQuestion.js";
|
|
4
|
+
import {processPrompt} from "../utils/processPrompt.js";
|
|
5
|
+
|
|
6
|
+
export async function password(question: NormalizedPromptQuestion, answers: Record<string, unknown>) {
|
|
7
|
+
return processPrompt(question, answers, () =>
|
|
8
|
+
clackPassword({
|
|
9
|
+
message: question.message,
|
|
10
|
+
mask: question.mask === false ? undefined : String(question.mask || "•")
|
|
11
|
+
})
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {normalizeChoices} from "../utils/normalizeChoices.js";
|
|
2
|
+
import {autocomplete} from "./autocomplete.js";
|
|
3
|
+
import {checkbox} from "./checkbox.js";
|
|
4
|
+
import {confirm} from "./confirm.js";
|
|
5
|
+
import {input} from "./input.js";
|
|
6
|
+
import {list} from "./list.js";
|
|
7
|
+
import {password} from "./password.js";
|
|
8
|
+
|
|
9
|
+
const SEARCH_ACTION = "__tsed_cli_search_again__";
|
|
10
|
+
|
|
11
|
+
const clack = vi.hoisted(() => ({
|
|
12
|
+
text: vi.fn(),
|
|
13
|
+
select: vi.fn(),
|
|
14
|
+
multiselect: vi.fn(),
|
|
15
|
+
confirm: vi.fn(),
|
|
16
|
+
password: vi.fn(),
|
|
17
|
+
note: vi.fn(),
|
|
18
|
+
isCancel: vi.fn().mockReturnValue(false),
|
|
19
|
+
cancel: vi.fn()
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("@clack/prompts", () => clack);
|
|
23
|
+
|
|
24
|
+
describe("prompt handlers", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
clack.text.mockReset();
|
|
27
|
+
clack.select.mockReset();
|
|
28
|
+
clack.multiselect.mockReset();
|
|
29
|
+
clack.confirm.mockReset();
|
|
30
|
+
clack.password.mockReset();
|
|
31
|
+
clack.note.mockReset();
|
|
32
|
+
clack.isCancel.mockReset().mockReturnValue(false);
|
|
33
|
+
clack.cancel.mockReset();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("input should resolve responses from clack text prompt", async () => {
|
|
37
|
+
clack.text.mockResolvedValue("Jane Doe");
|
|
38
|
+
|
|
39
|
+
const result = await input(
|
|
40
|
+
{
|
|
41
|
+
type: "input",
|
|
42
|
+
name: "fullName",
|
|
43
|
+
message: "Your name?",
|
|
44
|
+
default: "John"
|
|
45
|
+
},
|
|
46
|
+
{}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(clack.text).toHaveBeenCalledWith({message: "Your name?", initialValue: "John"});
|
|
50
|
+
expect(result).toBe("Jane Doe");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("password should mask the prompt by default", async () => {
|
|
54
|
+
clack.password.mockResolvedValue("secret");
|
|
55
|
+
|
|
56
|
+
const result = await password(
|
|
57
|
+
{
|
|
58
|
+
type: "password",
|
|
59
|
+
name: "token",
|
|
60
|
+
message: "Token?"
|
|
61
|
+
},
|
|
62
|
+
{}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(clack.password).toHaveBeenCalledWith({message: "Token?", mask: "•"});
|
|
66
|
+
expect(result).toBe("secret");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("confirm should pass boolean default as initialValue", async () => {
|
|
70
|
+
clack.confirm.mockResolvedValue(true);
|
|
71
|
+
|
|
72
|
+
const result = await confirm(
|
|
73
|
+
{
|
|
74
|
+
type: "confirm",
|
|
75
|
+
name: "proceed",
|
|
76
|
+
message: "Continue?",
|
|
77
|
+
default: false
|
|
78
|
+
},
|
|
79
|
+
{}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(clack.confirm).toHaveBeenCalledWith({message: "Continue?", initialValue: false});
|
|
83
|
+
expect(result).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("list should render normalized choices and return selection", async () => {
|
|
87
|
+
clack.select.mockResolvedValue("b");
|
|
88
|
+
const choices = normalizeChoices(["a", "b"]);
|
|
89
|
+
|
|
90
|
+
const result = await list(
|
|
91
|
+
{
|
|
92
|
+
type: "list",
|
|
93
|
+
name: "choice",
|
|
94
|
+
message: "Pick",
|
|
95
|
+
choices
|
|
96
|
+
},
|
|
97
|
+
{}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(clack.select).toHaveBeenCalledWith({
|
|
101
|
+
message: "Pick",
|
|
102
|
+
options: choices.map((choice) => ({label: choice.label, value: choice.value, hint: choice.hint})),
|
|
103
|
+
initialValue: "a",
|
|
104
|
+
maxItems: undefined
|
|
105
|
+
});
|
|
106
|
+
expect(result).toBe("b");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("checkbox should respect default values", async () => {
|
|
110
|
+
clack.multiselect.mockResolvedValue(["two"]);
|
|
111
|
+
|
|
112
|
+
const result = await checkbox(
|
|
113
|
+
{
|
|
114
|
+
type: "checkbox",
|
|
115
|
+
name: "features",
|
|
116
|
+
message: "Select",
|
|
117
|
+
choices: [
|
|
118
|
+
{label: "One", value: "one"},
|
|
119
|
+
{label: "Two", value: "two", checked: true}
|
|
120
|
+
],
|
|
121
|
+
default: ["two"]
|
|
122
|
+
},
|
|
123
|
+
{}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(clack.multiselect).toHaveBeenCalledWith({
|
|
127
|
+
message: "Select",
|
|
128
|
+
options: [
|
|
129
|
+
{label: "One", value: "one", hint: undefined},
|
|
130
|
+
{label: "Two", value: "two", hint: undefined}
|
|
131
|
+
],
|
|
132
|
+
initialValues: ["two"]
|
|
133
|
+
});
|
|
134
|
+
expect(result).toEqual(["two"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("list should throw when no choices are provided", async () => {
|
|
138
|
+
await expect(
|
|
139
|
+
list(
|
|
140
|
+
{
|
|
141
|
+
type: "list",
|
|
142
|
+
name: "empty",
|
|
143
|
+
message: "Empty",
|
|
144
|
+
choices: []
|
|
145
|
+
},
|
|
146
|
+
{}
|
|
147
|
+
)
|
|
148
|
+
).rejects.toThrow('Question "empty" does not provide any choices');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("autocomplete should support search flow and selection", async () => {
|
|
152
|
+
clack.select.mockResolvedValueOnce(SEARCH_ACTION).mockResolvedValueOnce("beta");
|
|
153
|
+
clack.text.mockResolvedValueOnce("b");
|
|
154
|
+
|
|
155
|
+
const result = await autocomplete(
|
|
156
|
+
{
|
|
157
|
+
type: "autocomplete",
|
|
158
|
+
name: "plugin",
|
|
159
|
+
message: "Plugin?",
|
|
160
|
+
pageSize: 5,
|
|
161
|
+
source: (answers, keyword = "") => {
|
|
162
|
+
expect(answers).toHaveProperty("prefilled", true);
|
|
163
|
+
return Promise.resolve(["alpha", "beta"].filter((entry) => entry.includes(keyword)));
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{prefilled: true}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(clack.text).toHaveBeenCalledWith({
|
|
170
|
+
message: "Plugin? (no matches, type to search)",
|
|
171
|
+
initialValue: ""
|
|
172
|
+
});
|
|
173
|
+
expect(result).toBe("beta");
|
|
174
|
+
});
|
|
175
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type {NormalizedChoice} from "../utils/normalizeChoices.js";
|
|
2
|
+
import type {PromptAutocompleteQuestion, PromptQuestion} from "./PromptQuestion.js";
|
|
3
|
+
|
|
4
|
+
export type NormalizedPromptQuestion = PromptQuestion & {
|
|
5
|
+
message: string;
|
|
6
|
+
choices?: NormalizedChoice[];
|
|
7
|
+
source?: PromptAutocompleteQuestion["source"];
|
|
8
|
+
default?: unknown;
|
|
9
|
+
mask?: string | boolean;
|
|
10
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enumerates the built-in prompt types supported by the Ts.ED CLI.
|
|
5
|
+
*/
|
|
6
|
+
export type PromptType = "input" | "password" | "confirm" | "list" | "checkbox" | "autocomplete";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents a single choice entry usable by select, checkbox, and autocomplete prompts.
|
|
10
|
+
*/
|
|
11
|
+
export interface PromptChoice<Value = any> {
|
|
12
|
+
/**
|
|
13
|
+
* Human friendly label displayed in the prompt list.
|
|
14
|
+
*/
|
|
15
|
+
name?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Raw value returned when the choice is selected.
|
|
18
|
+
*/
|
|
19
|
+
value?: Value;
|
|
20
|
+
/**
|
|
21
|
+
* Optional short label shown beside the main choice name.
|
|
22
|
+
*/
|
|
23
|
+
short?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Marks the choice as disabled (boolean or reason string).
|
|
26
|
+
*/
|
|
27
|
+
disabled?: boolean | string;
|
|
28
|
+
/**
|
|
29
|
+
* For checkbox prompts, marks the choice as checked by default.
|
|
30
|
+
*/
|
|
31
|
+
checked?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Choice definition accepted by prompts. A plain value will be coerced to a `PromptChoice`.
|
|
36
|
+
*/
|
|
37
|
+
export type PromptChoiceInput<Value = any> = PromptChoice<Value> | Value;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Transforms user input before it becomes part of the command context.
|
|
41
|
+
*/
|
|
42
|
+
export type PromptTransformer = (input: any, answers: Record<string, any>, flags?: {isFinal?: boolean}) => any;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validates the user input, returning `true`, `false`, or an error string.
|
|
46
|
+
*/
|
|
47
|
+
export type PromptValidator = (input: any, answers: Record<string, any>) => MaybePromise<boolean | string>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Filters the answer into a different representation before persistence.
|
|
51
|
+
*/
|
|
52
|
+
export type PromptFilter = (input: any, answers: Record<string, any>) => MaybePromise<any>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Determines whether a prompt should run.
|
|
56
|
+
*/
|
|
57
|
+
export type PromptWhen = boolean | ((answers: Record<string, any>) => MaybePromise<boolean>);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Base contract shared by every question type.
|
|
61
|
+
*/
|
|
62
|
+
export interface PromptBaseQuestion<Value = any> {
|
|
63
|
+
/**
|
|
64
|
+
* Prompt type to render.
|
|
65
|
+
*/
|
|
66
|
+
type: PromptType;
|
|
67
|
+
/**
|
|
68
|
+
* Unique answer key assigned to the prompt.
|
|
69
|
+
*/
|
|
70
|
+
name: string;
|
|
71
|
+
/**
|
|
72
|
+
* Prompt label. Accepts a string or function (resolved at runtime).
|
|
73
|
+
*/
|
|
74
|
+
message: string | ((answers: Record<string, any>) => MaybePromise<string>);
|
|
75
|
+
/**
|
|
76
|
+
* Allows skipping the prompt based on previous answers.
|
|
77
|
+
*/
|
|
78
|
+
when?: PromptWhen;
|
|
79
|
+
/**
|
|
80
|
+
* Default input value or factory function returning one.
|
|
81
|
+
*/
|
|
82
|
+
default?: Value | ((answers: Record<string, any>) => MaybePromise<Value>);
|
|
83
|
+
/**
|
|
84
|
+
* Mutates the visual input while the user types.
|
|
85
|
+
*/
|
|
86
|
+
transformer?: PromptTransformer;
|
|
87
|
+
/**
|
|
88
|
+
* Validates user input. Return `false`/string to display an error.
|
|
89
|
+
*/
|
|
90
|
+
validate?: PromptValidator;
|
|
91
|
+
/**
|
|
92
|
+
* Mutates the stored answer after validation.
|
|
93
|
+
*/
|
|
94
|
+
filter?: PromptFilter;
|
|
95
|
+
/**
|
|
96
|
+
* Optional max number of rows visible in select/checkbox prompts.
|
|
97
|
+
*/
|
|
98
|
+
pageSize?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Whether select prompts loop when reaching boundaries.
|
|
101
|
+
*/
|
|
102
|
+
loop?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Plain text prompt.
|
|
107
|
+
*/
|
|
108
|
+
export interface PromptInputQuestion extends PromptBaseQuestion<string> {
|
|
109
|
+
type: "input";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Hidden text prompt (e.g., passwords or tokens).
|
|
114
|
+
*/
|
|
115
|
+
export interface PromptPasswordQuestion extends PromptBaseQuestion<string> {
|
|
116
|
+
type: "password";
|
|
117
|
+
/**
|
|
118
|
+
* Character used to mask input (default: •). Set `false` to show raw input.
|
|
119
|
+
*/
|
|
120
|
+
mask?: string | boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Boolean confirmation prompt (yes/no).
|
|
125
|
+
*/
|
|
126
|
+
export interface PromptConfirmQuestion extends PromptBaseQuestion<boolean> {
|
|
127
|
+
type: "confirm";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Single-select prompt with predefined choices.
|
|
132
|
+
*/
|
|
133
|
+
export interface PromptListQuestion extends PromptBaseQuestion<any> {
|
|
134
|
+
type: "list";
|
|
135
|
+
/**
|
|
136
|
+
* Available choices displayed to the user.
|
|
137
|
+
*/
|
|
138
|
+
choices: PromptChoiceInput[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Multi-select prompt where the result is an array of chosen values.
|
|
143
|
+
*/
|
|
144
|
+
export interface PromptCheckboxQuestion extends PromptBaseQuestion<any[]> {
|
|
145
|
+
type: "checkbox";
|
|
146
|
+
/**
|
|
147
|
+
* Available choices displayed to the user.
|
|
148
|
+
*/
|
|
149
|
+
choices: PromptChoiceInput[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Searchable prompt that fetches choices dynamically.
|
|
154
|
+
*/
|
|
155
|
+
export interface PromptAutocompleteQuestion extends PromptBaseQuestion<any> {
|
|
156
|
+
type: "autocomplete";
|
|
157
|
+
/**
|
|
158
|
+
* Async loader returning the set of choices filtered by the keyword.
|
|
159
|
+
*/
|
|
160
|
+
source: (answers: Record<string, any>, keyword?: string) => MaybePromise<PromptChoiceInput[]>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Union describing every supported Ts.ED CLI question type.
|
|
165
|
+
*/
|
|
166
|
+
export type PromptQuestion =
|
|
167
|
+
| PromptInputQuestion
|
|
168
|
+
| PromptPasswordQuestion
|
|
169
|
+
| PromptConfirmQuestion
|
|
170
|
+
| PromptListQuestion
|
|
171
|
+
| PromptCheckboxQuestion
|
|
172
|
+
| PromptAutocompleteQuestion;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type {PromptQuestion} from "../interfaces/PromptQuestion.js";
|
|
2
|
+
|
|
3
|
+
export async function applyTransforms(question: PromptQuestion, answers: Record<string, any>, value: unknown) {
|
|
4
|
+
let next = value;
|
|
5
|
+
|
|
6
|
+
if (question.transformer) {
|
|
7
|
+
next = await question.transformer(next, answers, {isFinal: true});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (question.filter) {
|
|
11
|
+
next = await question.filter(next, answers);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return next;
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {cancel as cancelPrompt, isCancel} from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
import {PromptCancelledError} from "../errors/PromptCancelledError.js";
|
|
4
|
+
|
|
5
|
+
export function ensureNotCancelled<T>(value: T | symbol): T {
|
|
6
|
+
if (isCancel(value)) {
|
|
7
|
+
cancelPrompt();
|
|
8
|
+
throw new PromptCancelledError();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {isString} from "@tsed/core";
|
|
2
|
+
|
|
3
|
+
import type {PromptQuestion} from "../interfaces/PromptQuestion.js";
|
|
4
|
+
|
|
5
|
+
export async function getValidationError(question: PromptQuestion, answers: Record<string, unknown>, value: unknown) {
|
|
6
|
+
if (!question.validate) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const result = await question.validate(value, answers);
|
|
11
|
+
|
|
12
|
+
if (result === false) {
|
|
13
|
+
return "Invalid value.";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (isString(result)) {
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {PromptChoiceInput} from "../interfaces/PromptQuestion.js";
|
|
2
|
+
|
|
3
|
+
export type NormalizedChoice = {
|
|
4
|
+
label: string;
|
|
5
|
+
value: any;
|
|
6
|
+
hint?: string;
|
|
7
|
+
checked?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function normalizeChoices(inputs: PromptChoiceInput[] = []): NormalizedChoice[] {
|
|
11
|
+
return inputs.map((choice) => {
|
|
12
|
+
if (typeof choice === "object" && choice !== null && "value" in choice) {
|
|
13
|
+
const resolvedValue = choice.value ?? choice.name;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
label: choice.name ?? String(resolvedValue ?? ""),
|
|
17
|
+
value: resolvedValue,
|
|
18
|
+
hint: choice.short,
|
|
19
|
+
checked: choice.checked
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
label: String(choice),
|
|
25
|
+
value: choice
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {NormalizedPromptQuestion} from "../interfaces/NormalizedPromptQuestion.js";
|
|
2
|
+
import type {PromptQuestion} from "../interfaces/PromptQuestion.js";
|
|
3
|
+
import {normalizeChoices} from "./normalizeChoices.js";
|
|
4
|
+
import {resolveMaybe} from "./resolveMaybe.js";
|
|
5
|
+
|
|
6
|
+
export async function normalizeQuestion(question: PromptQuestion, answers: Record<string, unknown>) {
|
|
7
|
+
const normalized: NormalizedPromptQuestion = {
|
|
8
|
+
...(question as any),
|
|
9
|
+
name: question.name,
|
|
10
|
+
type: question.type,
|
|
11
|
+
message: await resolveMaybe(question.message, answers)
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
if ("default" in question && question.default !== undefined) {
|
|
15
|
+
normalized.default = await resolveMaybe(question.default, answers);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if ("choices" in question && question.choices?.length) {
|
|
19
|
+
normalized.choices = normalizeChoices([...question.choices]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if ("source" in question && question.source) {
|
|
23
|
+
const source = question.source;
|
|
24
|
+
|
|
25
|
+
normalized.source = (state: Record<string, any>, keyword?: string) => {
|
|
26
|
+
return source({...answers, ...state}, keyword);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {note} from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
import type {PromptQuestion} from "../interfaces/PromptQuestion.js";
|
|
4
|
+
import {applyTransforms} from "./applyTransforms.js";
|
|
5
|
+
import {ensureNotCancelled} from "./ensureNotCancelled.js";
|
|
6
|
+
import {getValidationError} from "./getValidationError.js";
|
|
7
|
+
|
|
8
|
+
export const CONTINUE = Symbol.for("prompt:continue");
|
|
9
|
+
type PromptExecutor = () => Promise<unknown> | unknown;
|
|
10
|
+
|
|
11
|
+
export async function processPrompt(question: PromptQuestion, answers: Record<string, unknown>, cb: PromptExecutor) {
|
|
12
|
+
while (true) {
|
|
13
|
+
const result = await cb();
|
|
14
|
+
|
|
15
|
+
if (result === CONTINUE) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const value = ensureNotCancelled(result);
|
|
20
|
+
const transformed = await applyTransforms(question, answers, value);
|
|
21
|
+
const error = await getValidationError(question, answers, transformed);
|
|
22
|
+
|
|
23
|
+
if (!error) {
|
|
24
|
+
return transformed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
note(error, "Validation error");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {PromptAutocompleteQuestion, PromptListQuestion} from "../interfaces/PromptQuestion.js";
|
|
2
|
+
import type {NormalizedChoice} from "./normalizeChoices.js";
|
|
3
|
+
|
|
4
|
+
export function resolveListDefault(
|
|
5
|
+
question: Pick<PromptListQuestion | PromptAutocompleteQuestion, "default">,
|
|
6
|
+
choices: NormalizedChoice[]
|
|
7
|
+
) {
|
|
8
|
+
if (question.default !== undefined) {
|
|
9
|
+
if (typeof question.default === "number") {
|
|
10
|
+
return choices[question.default]?.value ?? choices[0]?.value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return question.default;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const checked = choices.find((choice) => choice.checked);
|
|
17
|
+
if (checked) {
|
|
18
|
+
return checked.value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return choices[0]?.value;
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function resolveMaybe<T>(
|
|
2
|
+
value: T | ((answers: Record<string, any>) => T | Promise<T>) | undefined,
|
|
3
|
+
answers: Record<string, any>
|
|
4
|
+
): Promise<T> | T {
|
|
5
|
+
if (typeof value === "function") {
|
|
6
|
+
return (value as any)(answers) as Promise<T> | T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return value as T;
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type {PromptQuestion} from "../interfaces/PromptQuestion.js";
|
|
2
|
+
|
|
3
|
+
export async function shouldAsk(question: PromptQuestion, answers: Record<string, any>) {
|
|
4
|
+
if (question.when === undefined) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof question.when === "function") {
|
|
9
|
+
return !!(await question.when(answers));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return question.when;
|
|
13
|
+
}
|