@topcli/prompts 3.1.0 → 4.0.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/README.md CHANGED
@@ -55,6 +55,8 @@ Simple prompt, similar to `rl.question()` with an improved UI.
55
55
 
56
56
  Use `options.defaultValue` to set a default value.
57
57
 
58
+ Use `options.hint` to display a hint alongside the question.
59
+
58
60
  Use `options.secure` if you need to hide both input and answer. You can provide either a **boolean** or an **object** which allows to configure a `placeholder` such as `*`.
59
61
 
60
62
  Use `options.signal` to set an `AbortSignal` (throws a [AbortError](#aborterror)).
@@ -245,14 +247,30 @@ Use `options.skip` to skip prompt. It will return `options.initial` (`false` by
245
247
 
246
248
  The `PromptAgent` class allows to programmatically set the next answers for any prompt function, this can be useful for testing.
247
249
 
250
+ #### Shared instance
251
+
252
+ `PromptAgent.shared()` returns a singleton shared across all prompt calls that do not receive an explicit `agent` option. Suitable for simple test setups, but beware of state leaking between tests.
253
+
248
254
  ```ts
249
- const agent = PromptAgent.agent();
255
+ const agent = PromptAgent.shared();
250
256
  agent.nextAnswer("John");
251
257
 
252
258
  const input = await question("What's your name?");
253
259
  assert.equal(input, "John");
254
260
  ```
255
261
 
262
+ #### Individual instance
263
+
264
+ For better isolation (e.g. in unit tests), create a dedicated `PromptAgent` and pass it via the `agent` option. This avoids any state leaking from the shared instance.
265
+
266
+ ```ts
267
+ const agent = new PromptAgent();
268
+ agent.nextAnswer("John");
269
+
270
+ const input = await question("What's your name?", { agent });
271
+ assert.equal(input, "John");
272
+ ```
273
+
256
274
  > [!WARNING]
257
275
  > Answers set with `PromptAgent` will **bypass** any logical & validation rules.
258
276
  > Examples:
@@ -311,6 +329,7 @@ export type TransformationResponse<T> = InvalidResponse | ValidTransformationRes
311
329
 
312
330
  export interface QuestionOptions<T = string> extends AbstractPromptOptions {
313
331
  defaultValue?: string;
332
+ hint?: string;
314
333
  validators?: PromptValidator<string>[];
315
334
  transformer?: PromptTransformer<T>;
316
335
  secure?: boolean | { placeholder: string };
package/dist/index.d.mts CHANGED
@@ -34,8 +34,7 @@ declare class PromptAgent<T = string> {
34
34
  * When not empty, any prompt will be answered by the first answer in this list.
35
35
  */
36
36
  nextAnswers: T[];
37
- static agent<T>(): PromptAgent<T>;
38
- constructor(instancier: symbol);
37
+ static shared<T>(): PromptAgent<any>;
39
38
  /**
40
39
  * Programmatically set the next answer for any prompt (`question()`, `confirm()`, `select()`)
41
40
  *
@@ -43,14 +42,15 @@ declare class PromptAgent<T = string> {
43
42
  *
44
43
  * @example
45
44
  * ```js
46
- * const promptAgent = PromptAgent.agent();
45
+ * const promptAgent = PromptAgent.shared();
47
46
  * promptAgent.nextAnswer("toto");
48
47
  *
49
48
  * const input = await question("what is your name?");
50
49
  * assert.equal(input, "toto");
51
50
  * ```
52
51
  */
53
- nextAnswer(value: T): void;
52
+ nextAnswer(value: T | T[]): void;
53
+ clear(): void;
54
54
  }
55
55
  //#endregion
56
56
  //#region src/transformers.d.ts
@@ -83,11 +83,13 @@ interface AbstractPromptOptions {
83
83
  message: string;
84
84
  skip?: boolean;
85
85
  signal?: AbortSignal;
86
+ agent?: PromptAgent<string | boolean>;
86
87
  }
87
88
  //#endregion
88
89
  //#region src/prompts/question.d.ts
89
90
  interface QuestionOptions<T = string> extends AbstractPromptOptions {
90
91
  defaultValue?: string;
92
+ hint?: string;
91
93
  validators?: PromptValidator<string>[];
92
94
  transformer?: PromptTransformer<T>;
93
95
  secure?: boolean | {
@@ -135,5 +137,4 @@ declare const transformers: {
135
137
  url: typeof url;
136
138
  };
137
139
  //#endregion
138
- export { type AbstractPromptOptions, type Choice, type ConfirmOptions, type InvalidResponse, type InvalidResponseObject, type MultiselectOptions, PromptAgent, type PromptTransformer, type PromptValidator, type QuestionOptions, type SelectOptions, type Separator, type TransformationResponse, type ValidResponse, type ValidResponseObject, type ValidTransformationResponse, type ValidationResponse, type ValidationResponseObject, confirm, multiselect, question, select, transformers, validators };
139
- //# sourceMappingURL=index.d.mts.map
140
+ export { type AbstractPromptOptions, type Choice, type ConfirmOptions, type InvalidResponse, type InvalidResponseObject, type MultiselectOptions, PromptAgent, type PromptTransformer, type PromptValidator, type QuestionOptions, type SelectOptions, type Separator, type TransformationResponse, type ValidResponse, type ValidResponseObject, type ValidTransformationResponse, type ValidationResponse, type ValidationResponseObject, confirm, multiselect, question, select, transformers, validators };
package/dist/index.mjs CHANGED
@@ -28,22 +28,16 @@ function resultError(result) {
28
28
  }
29
29
  //#endregion
30
30
  //#region src/prompt-agent.ts
31
- const kPrivateInstancier = Symbol("instancier");
32
31
  var PromptAgent = class PromptAgent {
33
32
  /**
34
33
  * The prompts answers queue.
35
34
  * When not empty, any prompt will be answered by the first answer in this list.
36
35
  */
37
36
  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");
37
+ static #sharedInstance;
38
+ static shared() {
39
+ this.#sharedInstance ??= new PromptAgent();
40
+ return this.#sharedInstance;
47
41
  }
48
42
  /**
49
43
  * Programmatically set the next answer for any prompt (`question()`, `confirm()`, `select()`)
@@ -52,7 +46,7 @@ var PromptAgent = class PromptAgent {
52
46
  *
53
47
  * @example
54
48
  * ```js
55
- * const promptAgent = PromptAgent.agent();
49
+ * const promptAgent = PromptAgent.shared();
56
50
  * promptAgent.nextAnswer("toto");
57
51
  *
58
52
  * const input = await question("what is your name?");
@@ -60,11 +54,11 @@ var PromptAgent = class PromptAgent {
60
54
  * ```
61
55
  */
62
56
  nextAnswer(value) {
63
- if (Array.isArray(value)) {
64
- this.nextAnswers.push(...value);
65
- return;
66
- }
67
- this.nextAnswers.push(value);
57
+ if (Array.isArray(value)) this.nextAnswers.push(...value);
58
+ else this.nextAnswers.push(value);
59
+ }
60
+ clear() {
61
+ this.nextAnswers = [];
68
62
  }
69
63
  };
70
64
  //#endregion
@@ -105,28 +99,25 @@ function integer() {
105
99
  }
106
100
  function url() {
107
101
  return { transform(input) {
102
+ if (URL.canParse(input)) return {
103
+ isValid: true,
104
+ transformed: new URL(input)
105
+ };
108
106
  try {
107
+ const parsed = new URL(`https://${input}`);
108
+ if (!parsed.hostname.includes(".") && parsed.hostname !== "localhost") return {
109
+ isValid: false,
110
+ error: "invalid URL"
111
+ };
109
112
  return {
110
113
  isValid: true,
111
- transformed: new URL(input)
114
+ transformed: parsed
112
115
  };
113
116
  } 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
- }
117
+ return {
118
+ isValid: false,
119
+ error: "invalid URL"
120
+ };
130
121
  }
131
122
  } };
132
123
  }
@@ -157,7 +148,7 @@ var AbstractPrompt = class AbstractPrompt extends EventEmitter {
157
148
  constructor(options) {
158
149
  super();
159
150
  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;
151
+ const { stdin: input = process.stdin, stdout: output = process.stdout, message, signal, skip = false, agent = PromptAgent.shared() } = options;
161
152
  if (typeof message !== "string") throw new TypeError(`message must be string, ${typeof message} given.`);
162
153
  if (!output.isTTY) Object.assign(output, {
163
154
  moveCursor: () => void 0,
@@ -169,7 +160,7 @@ var AbstractPrompt = class AbstractPrompt extends EventEmitter {
169
160
  this.signal = signal;
170
161
  this.skip = skip;
171
162
  this.history = [];
172
- this.agent = PromptAgent.agent();
163
+ this.agent = agent;
173
164
  if (this.stdout.isTTY) this.stdin.setRawMode(true);
174
165
  this.rl = readline.createInterface({
175
166
  input,
@@ -270,6 +261,7 @@ const SYMBOLS = {
270
261
  //#region src/prompts/question.ts
271
262
  var QuestionPrompt = class extends AbstractPrompt {
272
263
  defaultValue;
264
+ hint;
273
265
  tip;
274
266
  questionSuffixError;
275
267
  answer;
@@ -280,11 +272,12 @@ var QuestionPrompt = class extends AbstractPrompt {
280
272
  #secure;
281
273
  #securePlaceholder = null;
282
274
  constructor(options) {
283
- const { defaultValue, validators = [], transformer, secure = false, ...baseOptions } = options;
275
+ const { defaultValue, hint, validators = [], transformer, secure = false, ...baseOptions } = options;
284
276
  super({ ...baseOptions });
285
277
  if (validators.length > 0 && transformer !== void 0) throw new Error("validators and transformer are mutually exclusive");
286
278
  if (defaultValue && typeof defaultValue !== "string") throw new TypeError("defaultValue must be a string");
287
279
  this.defaultValue = defaultValue;
280
+ this.hint = hint;
288
281
  this.tip = this.defaultValue ? ` (${this.defaultValue})` : "";
289
282
  this.#validators = validators;
290
283
  this.#transformer = transformer;
@@ -294,6 +287,9 @@ var QuestionPrompt = class extends AbstractPrompt {
294
287
  } else this.#secure = Boolean(secure);
295
288
  this.questionSuffixError = "";
296
289
  }
290
+ get formattedHint() {
291
+ return this.hint ? styleText("gray", this.hint) : "";
292
+ }
297
293
  #question() {
298
294
  const { resolve, promise } = Promise.withResolvers();
299
295
  const questionQuery = this.#getQuestionQuery();
@@ -307,10 +303,12 @@ var QuestionPrompt = class extends AbstractPrompt {
307
303
  return promise;
308
304
  }
309
305
  #getQuestionQuery() {
310
- return `${styleText("bold", `${SYMBOLS.QuestionMark} ${this.message}${this.tip}`)} ${this.questionSuffixError}`;
306
+ const hintPart = this.formattedHint ? ` ${this.formattedHint}` : "";
307
+ return `${styleText("bold", `${SYMBOLS.QuestionMark} ${this.message}${this.tip}`)}${hintPart} ${this.questionSuffixError}`;
311
308
  }
312
309
  #setQuestionSuffixError(error) {
313
- this.questionSuffixError = styleText("red", `[${error}] `);
310
+ const suffix = styleText("red", `[${error}] `);
311
+ this.questionSuffixError = suffix;
314
312
  }
315
313
  #writeAnswer() {
316
314
  const prefix = this.answer ? SYMBOLS.Tick : SYMBOLS.Cross;
@@ -320,7 +318,7 @@ var QuestionPrompt = class extends AbstractPrompt {
320
318
  this.write(`${prefix} ${styleText("bold", this.message)} ${SYMBOLS.Pointer} ${stylizedAnswer}${EOL}`);
321
319
  }
322
320
  #getValidatingQuery(dotCount) {
323
- return `${styleText("bold", `${SYMBOLS.QuestionMark} ${this.message}${this.tip}`)} ${styleText("yellow", `[validating${".".repeat(dotCount)}]`)}${EOL}`;
321
+ return `${styleText("bold", `${SYMBOLS.QuestionMark} ${this.message}${this.tip}`)}${this.formattedHint ? ` ${this.formattedHint}` : ""} ${styleText("yellow", `[validating${".".repeat(dotCount)}]`)}${EOL}`;
324
322
  }
325
323
  async #runTransformer() {
326
324
  const result = this.#transformer.transform(this.answer);
@@ -420,7 +418,7 @@ var QuestionPrompt = class extends AbstractPrompt {
420
418
  };
421
419
  //#endregion
422
420
  //#region src/prompts/confirm.ts
423
- const kToggleKeys = new Set([
421
+ const kToggleKeys = /* @__PURE__ */ new Set([
424
422
  "left",
425
423
  "right",
426
424
  "tab",
@@ -1171,5 +1169,3 @@ const transformers = {
1171
1169
  };
1172
1170
  //#endregion
1173
1171
  export { PromptAgent, confirm, multiselect, question, select, transformers, validators };
1174
-
1175
- //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@topcli/prompts",
3
- "version": "3.1.0",
3
+ "version": "4.0.0",
4
4
  "description": "Node.js user input library for command-line interfaces.",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build": "tsdown src/index.ts --format cjs,esm --dts --clean",
7
+ "build": "tsdown src/index.ts --dts --clean",
8
8
  "prepublishOnly": "npm run build",
9
9
  "test-only": "node --test ./test/**/*.test.ts",
10
- "test-types": "npm run build && tsd && attw --pack .",
10
+ "test-types": "npm run build && tsd && attw --pack . --profile esm-only",
11
11
  "test": "c8 -r html npm run test-only && npm run test-types",
12
12
  "lint": "eslint src test",
13
13
  "lint:fix": "eslint . --fix"
@@ -21,14 +21,8 @@
21
21
  "types": "./dist/index.d.mts",
22
22
  "exports": {
23
23
  ".": {
24
- "import": {
25
- "types": "./dist/index.d.mts",
26
- "default": "./dist/index.mjs"
27
- },
28
- "require": {
29
- "types": "./dist/index.d.cts",
30
- "default": "./dist/index.cjs"
31
- }
24
+ "types": "./dist/index.d.mts",
25
+ "default": "./dist/index.mjs"
32
26
  }
33
27
  },
34
28
  "keywords": [
@@ -45,10 +39,10 @@
45
39
  "@arethetypeswrong/cli": "^0.18.2",
46
40
  "@openally/config.eslint": "^2.2.0",
47
41
  "@openally/config.typescript": "^1.2.1",
48
- "@types/node": "^25.0.3",
42
+ "@types/node": "^26.0.1",
49
43
  "c8": "^11.0.0",
50
44
  "tsd": "^0.33.0",
51
- "tsdown": "0.21.7",
45
+ "tsdown": "0.22.3",
52
46
  "typescript": "^6.0.2"
53
47
  },
54
48
  "engines": {