@nan0web/ui-cli 1.1.1 → 2.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.
Files changed (115) hide show
  1. package/README.md +114 -207
  2. package/package.json +22 -12
  3. package/src/CLI.js +22 -30
  4. package/src/CLiMessage.js +2 -3
  5. package/src/Command.js +26 -24
  6. package/src/CommandError.js +3 -5
  7. package/src/CommandHelp.js +40 -36
  8. package/src/CommandMessage.js +56 -40
  9. package/src/CommandParser.js +27 -25
  10. package/src/InputAdapter.js +630 -90
  11. package/src/OutputAdapter.js +7 -8
  12. package/src/README.md.js +190 -316
  13. package/src/components/Alert.js +3 -6
  14. package/src/components/prompt/Autocomplete.js +12 -0
  15. package/src/components/prompt/Confirm.js +29 -0
  16. package/src/components/prompt/DateTime.js +26 -0
  17. package/src/components/prompt/Input.js +15 -0
  18. package/src/components/prompt/Mask.js +12 -0
  19. package/src/components/prompt/Multiselect.js +26 -0
  20. package/src/components/prompt/Next.js +8 -0
  21. package/src/components/prompt/Password.js +13 -0
  22. package/src/components/prompt/Pause.js +9 -0
  23. package/src/components/prompt/ProgressBar.js +16 -0
  24. package/src/components/prompt/Select.js +29 -0
  25. package/src/components/prompt/Slider.js +16 -0
  26. package/src/components/prompt/Spinner.js +29 -0
  27. package/src/components/prompt/Toggle.js +13 -0
  28. package/src/components/prompt/Tree.js +17 -0
  29. package/src/components/view/Alert.js +78 -0
  30. package/src/components/view/Badge.js +11 -0
  31. package/src/components/view/Nav.js +23 -0
  32. package/src/components/view/Table.js +12 -0
  33. package/src/components/view/Toast.js +9 -0
  34. package/src/core/Component.js +79 -0
  35. package/src/core/PropValidation.js +138 -0
  36. package/src/core/render.js +37 -0
  37. package/src/index.js +80 -41
  38. package/src/test/PlaygroundTest.js +37 -25
  39. package/src/test/index.js +2 -4
  40. package/src/ui/alert.js +58 -0
  41. package/src/ui/autocomplete.js +86 -0
  42. package/src/ui/badge.js +35 -0
  43. package/src/ui/confirm.js +49 -0
  44. package/src/ui/date-time.js +45 -0
  45. package/src/ui/form.js +120 -55
  46. package/src/ui/index.js +18 -4
  47. package/src/ui/input.js +79 -152
  48. package/src/ui/mask.js +132 -0
  49. package/src/ui/multiselect.js +59 -0
  50. package/src/ui/nav.js +74 -0
  51. package/src/ui/next.js +18 -13
  52. package/src/ui/progress.js +88 -0
  53. package/src/ui/select.js +49 -72
  54. package/src/ui/slider.js +154 -0
  55. package/src/ui/spinner.js +65 -0
  56. package/src/ui/table.js +163 -0
  57. package/src/ui/toast.js +34 -0
  58. package/src/ui/toggle.js +34 -0
  59. package/src/ui/tree.js +393 -0
  60. package/src/utils/parse.js +1 -1
  61. package/types/CLI.d.ts +5 -5
  62. package/types/CLiMessage.d.ts +1 -1
  63. package/types/Command.d.ts +2 -2
  64. package/types/CommandHelp.d.ts +3 -3
  65. package/types/CommandMessage.d.ts +8 -8
  66. package/types/CommandParser.d.ts +3 -3
  67. package/types/InputAdapter.d.ts +149 -15
  68. package/types/OutputAdapter.d.ts +1 -1
  69. package/types/README.md.d.ts +1 -1
  70. package/types/UiMessage.d.ts +31 -29
  71. package/types/components/prompt/Autocomplete.d.ts +6 -0
  72. package/types/components/prompt/Confirm.d.ts +6 -0
  73. package/types/components/prompt/DateTime.d.ts +6 -0
  74. package/types/components/prompt/Input.d.ts +6 -0
  75. package/types/components/prompt/Mask.d.ts +6 -0
  76. package/types/components/prompt/Multiselect.d.ts +6 -0
  77. package/types/components/prompt/Next.d.ts +6 -0
  78. package/types/components/prompt/Password.d.ts +6 -0
  79. package/types/components/prompt/Pause.d.ts +6 -0
  80. package/types/components/prompt/ProgressBar.d.ts +12 -0
  81. package/types/components/prompt/Select.d.ts +18 -0
  82. package/types/components/prompt/Slider.d.ts +6 -0
  83. package/types/components/prompt/Spinner.d.ts +21 -0
  84. package/types/components/prompt/Toggle.d.ts +6 -0
  85. package/types/components/prompt/Tree.d.ts +6 -0
  86. package/types/components/view/Alert.d.ts +21 -0
  87. package/types/components/view/Badge.d.ts +5 -0
  88. package/types/components/view/Nav.d.ts +15 -0
  89. package/types/components/view/Table.d.ts +10 -0
  90. package/types/components/view/Toast.d.ts +5 -0
  91. package/types/core/Component.d.ts +34 -0
  92. package/types/core/PropValidation.d.ts +48 -0
  93. package/types/core/render.d.ts +6 -0
  94. package/types/index.d.ts +47 -15
  95. package/types/test/PlaygroundTest.d.ts +12 -8
  96. package/types/test/index.d.ts +1 -1
  97. package/types/ui/alert.d.ts +14 -0
  98. package/types/ui/autocomplete.d.ts +20 -0
  99. package/types/ui/badge.d.ts +8 -0
  100. package/types/ui/confirm.d.ts +21 -0
  101. package/types/ui/date-time.d.ts +19 -0
  102. package/types/ui/form.d.ts +43 -12
  103. package/types/ui/index.d.ts +17 -2
  104. package/types/ui/input.d.ts +31 -74
  105. package/types/ui/mask.d.ts +29 -0
  106. package/types/ui/multiselect.d.ts +25 -0
  107. package/types/ui/nav.d.ts +27 -0
  108. package/types/ui/progress.d.ts +43 -0
  109. package/types/ui/select.d.ts +25 -64
  110. package/types/ui/slider.d.ts +23 -0
  111. package/types/ui/spinner.d.ts +28 -0
  112. package/types/ui/table.d.ts +28 -0
  113. package/types/ui/toast.d.ts +8 -0
  114. package/types/ui/toggle.d.ts +17 -0
  115. package/types/ui/tree.d.ts +48 -0
package/src/ui/mask.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Mask module – provides formatted input handling.
3
+ *
4
+ * @module ui/mask
5
+ */
6
+
7
+ import prompts from 'prompts'
8
+ import { CancelError } from '@nan0web/ui/core'
9
+ import { beep } from './input.js'
10
+
11
+ /**
12
+ * Validates and optionally formats user input based on a pattern.
13
+ * Pattern uses '#' for numbers and 'A' for letters.
14
+ * Example: '(###) ###-####'
15
+ *
16
+ * @param {Object} config
17
+ * @param {string} config.message - Prompt question
18
+ * @param {string} config.mask - Mask pattern (e.g., '###-###')
19
+ * @param {string} [config.placeholder] - Hint for the user
20
+ * @param {Function} [config.t] - Translation function
21
+ * @returns {Promise<{value: string, cancelled: boolean}>}
22
+ */
23
+ /**
24
+ * Cleans the input value by stripping non-alphanumerics and smart prefix.
25
+ */
26
+ export function cleanMaskInput(value, mask) {
27
+ let cleanValue = value.toString().replace(/[^a-zA-Z0-9]/g, '')
28
+
29
+ // Smart Prefix Detection:
30
+ // If the user typed the mask's static prefix (e.g. '38' for '+38'),
31
+ // remove it from the input to prevent duplication.
32
+ const firstPlaceholderIndex = mask.search(/[#0A]/)
33
+ if (firstPlaceholderIndex > 0) {
34
+ const maskPrefix = mask.substring(0, firstPlaceholderIndex).replace(/[^a-zA-Z0-9]/g, '')
35
+ if (maskPrefix && cleanValue.startsWith(maskPrefix)) {
36
+ // Special Check: If the user typed ONLY the prefix so far, don't strip it yet?
37
+ // Actually, removing it is safer to avoid "Format must be: +38 (38)" error midway?
38
+ // But for full validation, we strip it.
39
+ cleanValue = cleanValue.substring(maskPrefix.length)
40
+ }
41
+ }
42
+ return cleanValue
43
+ }
44
+
45
+ /**
46
+ * Formats a value according to the given mask.
47
+ * pattern: # = digit, A = letter, 0 = digit.
48
+ *
49
+ * @param {string} value
50
+ * @param {string} mask
51
+ * @returns {string}
52
+ */
53
+ export function formatMask(value, mask) {
54
+ let i = 0
55
+ let v = 0
56
+ let result = ''
57
+
58
+ const cleanValue = cleanMaskInput(value, mask)
59
+
60
+ while (i < mask.length && v < cleanValue.length) {
61
+ const maskChar = mask[i]
62
+ if (maskChar === '#' || maskChar === '0' || maskChar === 'A') {
63
+ result += cleanValue[v]
64
+ v++
65
+ } else {
66
+ result += maskChar
67
+ }
68
+ i++
69
+ }
70
+ return result
71
+ }
72
+
73
+ export async function mask(config) {
74
+ const { message, mask, placeholder, t } = config
75
+
76
+ const validate = (val) => {
77
+ if (!val) {
78
+ beep()
79
+ return 'Input is required'
80
+ }
81
+
82
+ // Support '#', '0' for numbers and 'A' for letters.
83
+ const cleanMask = mask.replace(/[^#0A]/g, '')
84
+ const cleanVal = cleanMaskInput(val, mask)
85
+
86
+ if (cleanVal.length !== cleanMask.length) {
87
+ beep()
88
+ return `${config.t ? config.t('Format must be:') : 'Format must be:'} ${mask}`
89
+ }
90
+
91
+ return true
92
+ }
93
+
94
+ /*
95
+ */
96
+
97
+ // Pre-format the placeholder if it exists so the default value is visual
98
+ const initialValue = placeholder ? formatMask(placeholder, mask) : undefined
99
+
100
+ const response = await prompts(
101
+ {
102
+ type: 'text',
103
+ name: 'value',
104
+ message,
105
+ initial: initialValue,
106
+ validate,
107
+ format: (val) => formatMask(val, mask),
108
+ },
109
+ {
110
+ onCancel: () => {
111
+ throw new CancelError()
112
+ },
113
+ }
114
+ )
115
+
116
+ // Ensure the returned value is always formatted
117
+ const formatted = formatMask(response.value, mask)
118
+
119
+ // Manual Stdout Override:
120
+ // prompts library might verify correctly but display raw input on the final line in some environments.
121
+ // We force a clean UI by removing the last line and printing our own.
122
+ if (process.stdout.isTTY) {
123
+ process.stdout.moveCursor(0, -1) // Move up one line
124
+ process.stdout.clearLine(0) // Clear the line
125
+ // Re-print the message and value manually with proper formatting
126
+ // Note: We use '✔' to match prompts style, or we can use our own if desired.
127
+ // prompts default style: '✔ Message … Value'
128
+ console.log(`✔ ${message} … ${formatted}`)
129
+ }
130
+
131
+ return { value: formatted, cancelled: false }
132
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Multiselect module – provides interactive multiple-choice selection list.
3
+ *
4
+ * @module ui/multiselect
5
+ */
6
+
7
+ import prompts from 'prompts'
8
+ import { CancelError } from '@nan0web/ui/core'
9
+
10
+ /**
11
+ * Interactive multiple selection with checkboxes.
12
+ *
13
+ * @param {Object} config
14
+ * @param {string} config.message - Prompt question
15
+ * @param {Array<string|Object>} config.options - List of choices
16
+ * @param {number} [config.limit=10] - Visible items limit
17
+ * @param {Array<any>} [config.initial=[]] - Initial selected values
18
+ * @param {string|boolean} [config.instructions] - Custom instructions
19
+ * @param {string} [config.hint] - Navigation hint
20
+ * @param {Function} [config.t] - Translation function
21
+ * @returns {Promise<{value: Array<any>, cancelled: boolean}>}
22
+ */
23
+ export async function multiselect(config) {
24
+ const { message, options, limit = 10, initial = [], t } = config
25
+
26
+ if (!Array.isArray(options) || options.length === 0) {
27
+ throw new Error('Options array is required and must not be empty')
28
+ }
29
+
30
+ const choices = options.map((el) => {
31
+ if (typeof el === 'string') {
32
+ return { title: el, value: el, selected: initial.includes(el) }
33
+ }
34
+ return {
35
+ title: el.label || el.title,
36
+ value: el.value,
37
+ selected: initial.includes(el.value),
38
+ }
39
+ })
40
+
41
+ const response = await prompts(
42
+ {
43
+ type: 'multiselect',
44
+ name: 'value',
45
+ message,
46
+ choices,
47
+ limit,
48
+ instructions: config.instructions !== undefined ? config.instructions : false,
49
+ hint: config.hint || (t ? t('hint.multiselect') : undefined),
50
+ },
51
+ {
52
+ onCancel: () => {
53
+ throw new CancelError()
54
+ },
55
+ }
56
+ )
57
+
58
+ return { value: response.value || [], cancelled: false }
59
+ }
package/src/ui/nav.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Navigation components – Breadcrumbs, Tabs, Steps.
3
+ *
4
+ * @module ui/nav
5
+ */
6
+
7
+ import Logger from '@nan0web/log'
8
+
9
+ /**
10
+ * Renders a breadcrumb trail.
11
+ *
12
+ * @param {string[]} items - List of path segments.
13
+ * @param {Object} [options]
14
+ * @param {string} [options.separator='›'] - Separator character.
15
+ * @returns {string} Styled string.
16
+ */
17
+ export function breadcrumbs(items, options = {}) {
18
+ const { separator = '›' } = options
19
+ const sepStyle = Logger.style(` ${separator} `, { color: Logger.DIM })
20
+
21
+ return items
22
+ .map((item, index) => {
23
+ const isLast = index === items.length - 1
24
+ // Last item usually bold or white, previous dimmed
25
+ return Logger.style(item, { color: isLast ? Logger.WHITE : Logger.DIM })
26
+ })
27
+ .join(sepStyle)
28
+ }
29
+
30
+ /**
31
+ * Renders a tab bar (visual only).
32
+ *
33
+ * @param {string[]} items - List of tab labels.
34
+ * @param {number} [active=0] - Index of active tab.
35
+ * @returns {string} Styled string.
36
+ */
37
+ export function tabs(items, active = 0) {
38
+ return items
39
+ .map((item, index) => {
40
+ const isActive = index === active
41
+ if (isActive) {
42
+ // Active: Inverse or highlighted
43
+ const bg = 'BLUE' // Logger looks up keys or constant values
44
+ return Logger.style(` ${item} `, { color: Logger.WHITE, bgColor: bg })
45
+ }
46
+ // Inactive: Dimmed or standard
47
+ return Logger.style(` ${item} `, { color: Logger.DIM })
48
+ })
49
+ .join(Logger.style('│', { color: Logger.DIM }))
50
+ }
51
+
52
+ /**
53
+ * Renders a step indicator (wizard).
54
+ *
55
+ * @param {string[]} items - List of step labels.
56
+ * @param {number} [current=0] - Index of current step.
57
+ * @returns {string} Styled string.
58
+ */
59
+ export function steps(items, current = 0) {
60
+ return items
61
+ .map((item, index) => {
62
+ if (index < current) {
63
+ // Completed
64
+ return Logger.style(`✔ ${item}`, { color: Logger.GREEN })
65
+ } else if (index === current) {
66
+ // Current
67
+ return Logger.style(`● ${item}`, { color: Logger.CYAN })
68
+ } else {
69
+ // Future
70
+ return Logger.style(`○ ${item}`, { color: Logger.DIM })
71
+ }
72
+ })
73
+ .join(Logger.style(' ─ ', { color: Logger.DIM }))
74
+ }
package/src/ui/next.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @module ui/next
5
5
  */
6
6
 
7
- import process from "node:process"
7
+ import process from 'node:process'
8
8
 
9
9
  /**
10
10
  * Pause execution for a given amount of milliseconds.
@@ -13,7 +13,7 @@ import process from "node:process"
13
13
  * @returns {Promise<true>} Resolves with `true` after the timeout.
14
14
  */
15
15
  export async function pause(ms) {
16
- return new Promise(resolve => setTimeout(() => resolve(true), ms))
16
+ return new Promise((resolve) => setTimeout(() => resolve(true), ms))
17
17
  }
18
18
 
19
19
  /**
@@ -24,21 +24,26 @@ export async function pause(ms) {
24
24
  * @throws {Error} If stdin is already in raw mode.
25
25
  */
26
26
  export async function next(conf = undefined) {
27
+ // Automated environments support via PLAY_DEMO_SEQUENCE
28
+ if (process.env.PLAY_DEMO_SEQUENCE) {
29
+ return Promise.resolve('[SIMULATED KEY]')
30
+ }
31
+
27
32
  return new Promise((resolve, reject) => {
28
33
  if (process.stdin.isRaw) {
29
- reject(new Error("stdin is already in raw mode"))
34
+ reject(new Error('stdin is already in raw mode'))
30
35
  return
31
36
  }
32
- let buffer = ""
37
+ let buffer = ''
33
38
 
34
- const onData = chunk => {
39
+ const onData = (chunk) => {
35
40
  const str = chunk.toString()
36
41
  buffer += str
37
42
 
38
43
  if (conf === undefined) {
39
44
  cleanup()
40
45
  resolve(str)
41
- } else if (typeof conf === "string") {
46
+ } else if (typeof conf === 'string') {
42
47
  if (buffer === conf || buffer.endsWith(conf)) {
43
48
  cleanup()
44
49
  resolve(buffer)
@@ -54,21 +59,21 @@ export async function next(conf = undefined) {
54
59
  }
55
60
  }
56
61
 
57
- const errorHandler = err => {
62
+ const errorHandler = (err) => {
58
63
  cleanup()
59
64
  reject(err)
60
65
  }
61
66
 
62
67
  const cleanup = () => {
63
- process.stdin.off("data", onData)
64
- process.stdin.off("error", errorHandler)
68
+ process.stdin.off('data', onData)
69
+ process.stdin.off('error', errorHandler)
65
70
  process.stdin.setRawMode(false)
66
- process.stdin.resume()
71
+ process.stdin.pause()
67
72
  }
68
73
 
69
74
  process.stdin.setRawMode(true)
70
75
  process.stdin.resume()
71
- process.stdin.once("error", errorHandler)
72
- process.stdin.on("data", onData)
76
+ process.stdin.once('error', errorHandler)
77
+ process.stdin.on('data', onData)
73
78
  })
74
- }
79
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Progress module – visual progress bar with timers.
3
+ * @module ui/progress
4
+ */
5
+
6
+ /**
7
+ * Renders a progress bar.
8
+ */
9
+ export class ProgressBar {
10
+ /**
11
+ * @param {Object} options
12
+ * @param {number} options.total
13
+ * @param {string} [options.title]
14
+ * @param {number} [options.width=40]
15
+ */
16
+ constructor(options) {
17
+ this.total = options.total
18
+ this.title = options.title
19
+ this.width = options.width || 40
20
+ this.current = 0
21
+ this.startTime = Date.now()
22
+ }
23
+
24
+ /**
25
+ * Update progress.
26
+ * @param {number} current
27
+ */
28
+ update(current) {
29
+ this.current = current
30
+ this.render()
31
+ }
32
+
33
+ /**
34
+ * Increment progress.
35
+ * @param {number} [step=1]
36
+ */
37
+ tick(step = 1) {
38
+ this.current += step
39
+ this.render()
40
+ }
41
+
42
+ render() {
43
+ const percent = Math.min(100, Math.max(0, (this.current / this.total) * 100))
44
+ const filledWidth = Math.round((this.width * percent) / 100)
45
+ const emptyWidth = this.width - filledWidth
46
+
47
+ const elapsed = (Date.now() - this.startTime) / 1000
48
+ const rate = this.current / elapsed
49
+ const remaining = rate > 0 ? (this.total - this.current) / rate : 0
50
+
51
+ const bar =
52
+ '='.repeat(filledWidth) +
53
+ (filledWidth < this.width ? '>' : '') +
54
+ '-'.repeat(Math.max(0, emptyWidth - 1))
55
+
56
+ const timeStr = `[${this.formatTime(elapsed)} < ${this.formatTime(remaining)}]`
57
+
58
+ const output = `\r${this.title ? this.title + ' ' : ''}[${bar}] ${percent.toFixed(0)}% ${timeStr}`
59
+ process.stdout.write(output)
60
+
61
+ if (this.current >= this.total) {
62
+ process.stdout.write('\n')
63
+ }
64
+ }
65
+
66
+ formatTime(seconds) {
67
+ if (!isFinite(seconds) || seconds < 0) return '--:--'
68
+ const h = Math.floor(seconds / 3600)
69
+ const m = Math.floor((seconds % 3600) / 60)
70
+ const s = Math.floor(seconds % 60)
71
+
72
+ const parts = []
73
+ if (h > 0) parts.push(h.toString().padStart(2, '0'))
74
+ parts.push(m.toString().padStart(2, '0'))
75
+ parts.push(s.toString().padStart(2, '0'))
76
+
77
+ return parts.join(':')
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Functional helper for progress.
83
+ * @param {Object} options
84
+ * @returns {ProgressBar}
85
+ */
86
+ export function progress(options) {
87
+ return new ProgressBar(options)
88
+ }
package/src/ui/select.js CHANGED
@@ -4,97 +4,74 @@
4
4
  * @module ui/select
5
5
  */
6
6
 
7
- import { CancelError } from "@nan0web/ui/core"
8
- import createInput from "./input.js"
9
-
10
- /** @typedef {import("./input.js").Input} Input */
11
- /** @typedef {import("./input.js").InputFn} InputFn */
12
-
13
- /**
14
- * @typedef {Object} ConsoleLike
15
- * @property {(...args: any[]) => void} debug
16
- * @property {(...args: any[]) => void} log
17
- * @property {(...args: any[]) => void} info
18
- * @property {(...args: any[]) => void} warn
19
- * @property {(...args: any[]) => void} error
20
- */
21
-
22
- /**
23
- * @typedef {Object} SelectConfig
24
- * @property {string} title – Title displayed above the options list.
25
- * @property {string} prompt – Prompt displayed for the answer.
26
- * @property {Array|Map} options – Collection of selectable items.
27
- * @property {ConsoleLike} console – Console‑like object with an `info` method.
28
- * @property {string[]} [stops=[]] Words that trigger cancellation.
29
- * @property {InputFn} [ask] Custom ask function (defaults to {@link createInput}).
30
- * @property {string} [invalidPrompt="Invalid choice, try again: "] Message shown on invalid input.
31
- */
7
+ import prompts from 'prompts'
8
+ import { CancelError } from '@nan0web/ui/core'
9
+ import { validateString, validateFunction, validateNumber } from '../core/PropValidation.js'
32
10
 
33
11
  /**
34
12
  * Configuration object for {@link select}.
35
13
  *
36
- * @param {SelectConfig} input
37
- * @returns {Promise<{index:number,value:any}>} Resolves with the selected index and its value.
14
+ * @param {Object} input
15
+ * @param {string} input.title - Title displayed above the options list.
16
+ * @param {string} [input.prompt] - Prompt displayed for the answer.
17
+ * @param {Array|Map} input.options - Collection of selectable items.
18
+ * @param {Object} [input.console] - Deprecated. Ignored in new implementation.
19
+ * @param {string[]} [input.stops=[]] - Deprecated. Ignored in new implementation.
20
+ * @param {any} [input.ask] - Deprecated. Ignored in new implementation.
21
+ * @param {string} [input.invalidPrompt] - Deprecated. Ignored in new implementation.
22
+ * @param {number} [input.limit=10] - Max visible items.
23
+ * @param {string} [input.hint] - Hint text.
24
+ * @param {Function} [input.t] - Translation function.
25
+ * @returns {Promise<{index:number,value:any,cancelled:boolean}>} Resolves with the selected index and its value.
38
26
  *
39
27
  * @throws {CancelError} When the user cancels the operation.
40
- * @throws {Error} When options are missing or an incorrect value is supplied and no
41
- * `invalidPrompt` is defined.
42
28
  */
43
29
  export async function select(input) {
44
- const {
45
- title,
46
- prompt,
47
- invalidPrompt = "Invalid choice, try again: ",
48
- options: initOptins,
49
- console,
50
- stops = [],
51
- ask: initAsk,
52
- } = input
30
+ const { title, prompt, options: initOptins, limit = 30 } = input
31
+
32
+ // Prop Validation
33
+ validateString(title || prompt, 'title', 'Select', true)
34
+ validateNumber(limit, 'limit', 'Select')
35
+ validateFunction(input.t, 't', 'Select')
36
+ validateString(input.hint, 'hint', 'Select')
37
+
53
38
  let options = initOptins
54
- /** @type {InputFn} */
55
- const ask = initAsk ?? createInput(stops)
56
39
 
57
40
  // Normalise Map → Array of {label,value}
58
41
  if (options instanceof Map) {
59
- options = Array.from(options.entries()).map(
60
- ([value, label]) => ({ label, value }))
42
+ options = Array.from(options.entries()).map(([value, label]) => ({ label, value }))
61
43
  }
62
44
  if (!Array.isArray(options) || options.length === 0) {
63
- throw new Error("Options array is required and must not be empty")
45
+ throw new Error('Options array is required and must not be empty')
64
46
  }
65
- const list = options.map(el =>
66
- typeof el === "string" ? { label: el, value: el } : el,
67
- )
68
-
69
- console.info(title)
70
- list.forEach(({ label }, i) => console.info(` ${i + 1}) ${label}`))
71
-
72
- /**
73
- * Validation function passed to `ask` as the *loop* argument.
74
- * @type {import("./input.js").LoopFn}
75
- */
76
- const validator = async (input) => {
77
- if (input.cancelled) {
78
- throw new CancelError()
47
+ // Prepare options for prompts
48
+ const choices = options.map((el) => {
49
+ if (typeof el === 'string') {
50
+ return { title: el, value: el }
79
51
  }
80
- const idx = Number(input.value) - 1
81
- if (isNaN(idx) || idx < 0 || idx >= list.length) {
82
- if (invalidPrompt) {
83
- return true // repeat asking
84
- }
85
- throw new Error("Incorrect value provided")
52
+ return { title: el.label || el.title, value: el.value }
53
+ })
54
+
55
+ const response = await prompts(
56
+ {
57
+ type: 'select',
58
+ name: 'value',
59
+ message: title ? title : prompt,
60
+ choices: choices,
61
+ hint: input.hint || (input.t ? input.t('hint.select') : undefined),
62
+ instructions: false,
63
+ limit,
64
+ },
65
+ {
66
+ onCancel: () => {
67
+ throw new CancelError()
68
+ },
86
69
  }
87
- // valid selection – store index for later return
88
- // we reuse `idx` after ask resolves
89
- return false // stop looping
90
- }
70
+ )
91
71
 
92
- // Ask with validator loop; when validator returns false we have a valid answer.
93
- const answer = await ask(prompt, validator, invalidPrompt)
72
+ const index = choices.findIndex((c) => c.value === response.value)
94
73
 
95
- // After validator passes, compute the final index once more (safe)
96
- const finalIdx = Number(answer.value) - 1
97
- return { index: finalIdx, value: list[finalIdx].value }
74
+ return { index, value: response.value, cancelled: response.value === undefined }
98
75
  }
99
76
 
100
77
  /**