@nan0web/ui-cli 1.1.0 → 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 -29
  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
@@ -0,0 +1,29 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { confirm as baseConfirm } from '../../ui/confirm.js'
3
+ import { validateString, validateBoolean } from '../../core/PropValidation.js'
4
+
5
+ export function Confirm(props) {
6
+ return createPrompt('Confirm', props, async (p) => {
7
+ // Validate props
8
+ validateString(p.message || p.children, 'message/children', 'Confirm', true)
9
+ validateBoolean(p.initial, 'initial', 'Confirm')
10
+
11
+ const yesLabel = p.t ? p.t('yes') : 'yes'
12
+ const noLabel = p.t ? p.t('no') : 'no'
13
+
14
+ const result = await baseConfirm({
15
+ message: p.message || p.children,
16
+ initial: p.initial,
17
+ format: (val) => (val ? yesLabel : noLabel),
18
+ active: yesLabel,
19
+ inactive: noLabel,
20
+ })
21
+
22
+ // Restore boolean value from formatted string
23
+ if (typeof result.value === 'string') {
24
+ result.value = result.value === yesLabel
25
+ }
26
+
27
+ return result
28
+ })
29
+ }
@@ -0,0 +1,26 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { datetime as baseDateTime } from '../../ui/date-time.js'
3
+ import { validateDate, validateString, validateFunction } from '../../core/PropValidation.js'
4
+
5
+ export function DateTime(props) {
6
+ return createPrompt('DateTime', props, async (p) => {
7
+ // Validate props
8
+ validateString(p.message || p.label, 'message/label', 'DateTime', true)
9
+ validateDate(p.initial, 'initial', 'DateTime')
10
+ validateString(p.mask, 'mask', 'DateTime')
11
+ validateFunction(p.t, 't', 'DateTime')
12
+
13
+ // Auto-convert initial to Date if it's a string
14
+ let initial = p.initial
15
+ if (typeof initial === 'string') {
16
+ initial = new Date(initial)
17
+ }
18
+
19
+ return await baseDateTime({
20
+ message: p.message || p.label,
21
+ initial: initial,
22
+ mask: p.mask,
23
+ t: p.t,
24
+ })
25
+ })
26
+ }
@@ -0,0 +1,15 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { text } from '../../ui/input.js'
3
+
4
+ export function Input(props) {
5
+ return createPrompt('Input', props, async (p) => {
6
+ const config = {
7
+ message: p.message || p.label,
8
+ initial: p.initial || p.defaultValue,
9
+ type: p.type || 'text',
10
+ validate: p.validate || p.validator,
11
+ format: p.format,
12
+ }
13
+ return await text(config)
14
+ })
15
+ }
@@ -0,0 +1,12 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { mask as baseMask } from '../../ui/mask.js'
3
+
4
+ export function Mask(props) {
5
+ return createPrompt('Mask', props, async (p) => {
6
+ return await baseMask({
7
+ message: p.message || p.label,
8
+ mask: p.mask,
9
+ placeholder: p.placeholder,
10
+ })
11
+ })
12
+ }
@@ -0,0 +1,26 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { multiselect as baseMultiselect } from '../../ui/multiselect.js'
3
+
4
+ function getMultiselectInstructions(t) {
5
+ if (!t) return undefined
6
+ return (
7
+ `\n${t('Instructions')}:\n` +
8
+ ` ↑/↓: ${t('Highlight option')}\n` +
9
+ ` ←/→/[space]: ${t('Toggle selection')}\n` +
10
+ ` a: ${t('Toggle all')}\n` +
11
+ ` enter/return: ${t('Complete answer')}`
12
+ )
13
+ }
14
+
15
+ export function Multiselect(props) {
16
+ return createPrompt('Multiselect', props, async (p) => {
17
+ return await baseMultiselect({
18
+ message: p.message || p.label,
19
+ options: p.options,
20
+ limit: p.limit,
21
+ initial: p.initial,
22
+ instructions: p.instructions !== undefined ? p.instructions : getMultiselectInstructions(p.t),
23
+ hint: p.hint || (p.t ? p.t('hint.multiselect') : undefined),
24
+ })
25
+ })
26
+ }
@@ -0,0 +1,8 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { next } from '../../ui/next.js'
3
+
4
+ export function Next(props) {
5
+ return createPrompt('Next', props, async (p) => {
6
+ return await next(p.conf || p.keys)
7
+ })
8
+ }
@@ -0,0 +1,13 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { text } from '../../ui/input.js'
3
+
4
+ export function Password(props) {
5
+ return createPrompt('Password', props, async (p) => {
6
+ return await text({
7
+ message: p.message || p.label,
8
+ initial: p.initial,
9
+ type: 'password',
10
+ validate: p.validate || p.validator,
11
+ })
12
+ })
13
+ }
@@ -0,0 +1,9 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { pause } from '../../ui/next.js'
3
+
4
+ export function Pause(props) {
5
+ if (typeof props === 'number') props = { ms: props }
6
+ return createPrompt('Pause', props, async (p) => {
7
+ return await pause(p.ms || 1000)
8
+ })
9
+ }
@@ -0,0 +1,16 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { progress as baseProgress } from '../../ui/progress.js'
3
+
4
+ /**
5
+ * ProgressBar Component.
6
+ * usage:
7
+ * const bar = await render(ProgressBar({ total: 100 }));
8
+ * bar.update(50);
9
+ */
10
+ export function ProgressBar(props) {
11
+ return createPrompt('ProgressBar', props, async (p) => {
12
+ const bar = baseProgress(p)
13
+ // If action is provided, we could auto-run it, but progress bars usually strictly driven
14
+ return bar
15
+ })
16
+ }
@@ -0,0 +1,29 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { select } from '../../ui/select.js'
3
+
4
+ /**
5
+ * Select Prompt Component.
6
+ *
7
+ * @param {Object} props
8
+ * @param {string} props.message - Question/Title.
9
+ * @param {Array} props.options - Options list.
10
+ * @param {number} [props.limit] - Max visible options.
11
+ */
12
+ export function Select(props) {
13
+ return createPrompt('Select', props, async (p) => {
14
+ // Map props to legacy select config
15
+ // New API uses 'message', 'children'?, 'options'
16
+ // Legacy used 'title', 'prompt', 'options'
17
+
18
+ const config = {
19
+ title: p.title || p.message, // Support both
20
+ options: p.options || p.children, // Support children as options?
21
+ limit: p.limit,
22
+ hint: p.hint || (p.t ? p.t('hint.select') : undefined),
23
+ // Pass other potential legacy props
24
+ ...p,
25
+ }
26
+
27
+ return await select(config)
28
+ })
29
+ }
@@ -0,0 +1,16 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { slider as baseSlider } from '../../ui/slider.js'
3
+
4
+ export function Slider(props) {
5
+ return createPrompt('Slider', props, async (p) => {
6
+ return await baseSlider({
7
+ message: p.message || p.label,
8
+ initial: p.initial,
9
+ min: p.min,
10
+ max: p.max,
11
+ step: p.step,
12
+ jump: p.jump,
13
+ t: p.t,
14
+ })
15
+ })
16
+ }
@@ -0,0 +1,29 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { spinner as baseSpinner } from '../../ui/spinner.js'
3
+
4
+ /**
5
+ * Spinner Component.
6
+ * Usage: await render(Spinner({ message: 'Loading...', action: promise }))
7
+ *
8
+ * @param {Object} props
9
+ * @param {string} props.message - Main message.
10
+ * @param {Promise<any>} [props.action] - Async action to wait for.
11
+ * @param {string} [props.successMessage] - Message on success.
12
+ * @param {string} [props.errorMessage] - Message on error.
13
+ */
14
+ export function Spinner(props) {
15
+ return createPrompt('Spinner', props, async (p) => {
16
+ const spin = baseSpinner(p.message)
17
+ if (p.action && p.action.then) {
18
+ try {
19
+ const res = await p.action
20
+ spin.success(p.successMessage || 'Done')
21
+ return res
22
+ } catch (err) {
23
+ spin.error(p.errorMessage || 'Error')
24
+ throw err
25
+ }
26
+ }
27
+ return spin // Return controller if no action (legacy behavior)
28
+ })
29
+ }
@@ -0,0 +1,13 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { toggle as baseToggle } from '../../ui/toggle.js'
3
+
4
+ export function Toggle(props) {
5
+ return createPrompt('Toggle', props, async (p) => {
6
+ return await baseToggle({
7
+ message: p.message || p.children,
8
+ initial: p.initial,
9
+ active: p.active,
10
+ inactive: p.inactive,
11
+ })
12
+ })
13
+ }
@@ -0,0 +1,17 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { tree } from '../../ui/tree.js'
3
+
4
+ export function Tree(props) {
5
+ return createPrompt('Tree', props, async (p) => {
6
+ return await tree({
7
+ message: p.message || p.title,
8
+ mode: p.mode,
9
+ tree: p.tree || p.options || p.data,
10
+ loader: p.loader,
11
+ limit: p.limit,
12
+ initialExpanded: p.expanded,
13
+ multiselect: p.multiselect,
14
+ t: p.t,
15
+ })
16
+ })
17
+ }
@@ -0,0 +1,78 @@
1
+ import { createView } from '../../core/Component.js'
2
+ import Logger from '@nan0web/log'
3
+ import { beep } from '../../ui/input.js'
4
+
5
+ /**
6
+ * Alert View Component.
7
+ *
8
+ * @param {Object} props
9
+ * @param {string} props.title - Title of the alert.
10
+ * @param {string} props.children - Message content.
11
+ * @param {'info'|'success'|'warning'|'error'} [props.variant='info'] - Style variant.
12
+ * @param {string} [props.message] - Alias for children (legacy support).
13
+ * @param {boolean} [props.sound] - Play sound (side-effect during toString is acceptable here as it invokes on print).
14
+ */
15
+ export function Alert(props) {
16
+ // Normalize props (allow string argument as children shorthand)
17
+ const defaults = { title: '', children: '', variant: 'info' }
18
+
19
+ // If props is string, treat as children, otherwise merge with defaults
20
+ const safeProps =
21
+ typeof props === 'string' ? { ...defaults, children: props } : { ...defaults, ...props }
22
+
23
+ const { title, children, message = children, variant } = safeProps
24
+
25
+ return createView('Alert', props, () => {
26
+ // Logic extracted from old alert.js
27
+ const sound = props.sound || variant === 'error' || variant === 'warning'
28
+
29
+ // Note: beep() is technically a side effect.
30
+ // In "toString" pattern, side effects happen when string conversion receives focus.
31
+ // It's acceptable for sound, but we should be careful.
32
+ if (sound) beep()
33
+
34
+ const colors = {
35
+ info: Logger.CYAN,
36
+ success: Logger.GREEN,
37
+ warning: Logger.YELLOW,
38
+ error: Logger.RED,
39
+ }
40
+
41
+ const color = colors[variant] || Logger.WHITE
42
+ const icon =
43
+ {
44
+ info: 'ℹ',
45
+ success: '✔',
46
+ warning: '⚠',
47
+ error: '✖',
48
+ }[variant] || '•'
49
+
50
+ const msgStr = String(message || '')
51
+ const lines = msgStr.split('\n')
52
+ const paddedLines = lines.map((line) => ` ${line}`)
53
+
54
+ // Calculate max line length for border (at least 60 chars or based on content)
55
+ const contentLengths = [title ? title.length + 6 : 0, ...paddedLines.map((l) => l.length)]
56
+ const maxContentLen = Math.max(...contentLengths)
57
+ const len = Math.max(60, maxContentLen + 2)
58
+
59
+ const border = Logger.style('━'.repeat(len), { color })
60
+ let out = ''
61
+
62
+ out += `\n${border}\n`
63
+ if (title) {
64
+ out += Logger.style(` ${icon} ${title} `, { color }) + ' \n'
65
+ out += Logger.style('─'.repeat(len) + '\n', { color: Logger.DIM })
66
+ }
67
+ lines.forEach((line) => {
68
+ if (line.trim()) {
69
+ out += ' ' + Logger.style(line.trim(), { color }) + '\n'
70
+ } else {
71
+ out += '\n'
72
+ }
73
+ })
74
+ out += `${border}\n`
75
+
76
+ return out
77
+ })
78
+ }
@@ -0,0 +1,11 @@
1
+ import { createView } from '../../core/Component.js'
2
+ import { badge as baseBadge } from '../../ui/badge.js'
3
+
4
+ export function Badge(props) {
5
+ if (typeof props === 'string') {
6
+ props = { label: props }
7
+ }
8
+ return createView('Badge', props, (p) => {
9
+ return baseBadge(p.label, p.variant)
10
+ })
11
+ }
@@ -0,0 +1,23 @@
1
+ import { createView } from '../../core/Component.js'
2
+ import { breadcrumbs, tabs, steps } from '../../ui/nav.js'
3
+
4
+ export function Breadcrumbs(props) {
5
+ if (Array.isArray(props)) props = { items: props }
6
+ return createView('Breadcrumbs', props, (p) => {
7
+ return breadcrumbs(p.items, p.options)
8
+ })
9
+ }
10
+
11
+ export function Tabs(props) {
12
+ if (Array.isArray(props)) props = { items: props }
13
+ return createView('Tabs', props, (p) => {
14
+ return tabs(p.items, p.active)
15
+ })
16
+ }
17
+
18
+ export function Steps(props) {
19
+ if (Array.isArray(props)) props = { items: props }
20
+ return createView('Steps', props, (p) => {
21
+ return steps(p.items, p.current)
22
+ })
23
+ }
@@ -0,0 +1,12 @@
1
+ import { createPrompt } from '../../core/Component.js'
2
+ import { table as baseTable } from '../../ui/table.js'
3
+
4
+ /**
5
+ * Table Component.
6
+ * Can be static (display only) or interactive (filter/select).
7
+ */
8
+ export function Table(props) {
9
+ return createPrompt('Table', props, async (p) => {
10
+ return await baseTable(p)
11
+ })
12
+ }
@@ -0,0 +1,9 @@
1
+ import { createView } from '../../core/Component.js'
2
+ import { toast as baseToast } from '../../ui/toast.js'
3
+
4
+ export function Toast(props) {
5
+ if (typeof props === 'string') props = { message: props, variant: 'info' }
6
+ return createView('Toast', props, (p) => {
7
+ return baseToast(p.message, p.variant)
8
+ })
9
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Core Component logic.
3
+ *
4
+ * Defines the contract for View (static) and Prompt (interactive) components.
5
+ */
6
+
7
+ export const ComponentSymbol = Symbol.for('ui.component')
8
+ export const PromptSymbol = Symbol.for('ui.prompt')
9
+
10
+ /**
11
+ * Creates a Static View Component.
12
+ * These components are synchronous and can be stringified directly.
13
+ *
14
+ * @param {string} displayName - Name of the component (e.g. 'Alert').
15
+ * @param {any} props - Props passed to the component.
16
+ * @param {Function} formatFn - Pure function (props) => string.
17
+ */
18
+ export function createView(displayName, props, formatFn) {
19
+ const component = {
20
+ $$typeof: ComponentSymbol,
21
+ type: displayName,
22
+ props,
23
+ }
24
+
25
+ // Magic: toString calls the formatter
26
+ // We also implement nodejs.util.inspect.custom so console.log works directly
27
+ const toString = () => {
28
+ try {
29
+ return formatFn(props)
30
+ } catch (originalError) {
31
+ const e = /** @type {Error} */ (originalError)
32
+ return `[${displayName} Error: ${e.message}]`
33
+ }
34
+ }
35
+
36
+ Object.defineProperty(component, 'toString', {
37
+ value: toString,
38
+ enumerable: false,
39
+ })
40
+
41
+ Object.defineProperty(component, Symbol.for('nodejs.util.inspect.custom'), {
42
+ value: () => toString(),
43
+ enumerable: false,
44
+ })
45
+
46
+ return component
47
+ }
48
+
49
+ /**
50
+ * Creates an Interactive Prompt Component.
51
+ * These components are asynchronous and require `render()` or `await` handling.
52
+ *
53
+ * @param {string} displayName - Name of the component.
54
+ * @param {any} props - Props.
55
+ * @param {Function} executorFn - Async function (props) => Promise<result>.
56
+ */
57
+ export function createPrompt(displayName, props, executorFn) {
58
+ const component = {
59
+ $$typeof: PromptSymbol,
60
+ type: displayName,
61
+ props,
62
+ execute: () => executorFn(props),
63
+ }
64
+
65
+ // Prompts cannot be stringified meaningfully
66
+ const toString = () => `[Prompt: ${displayName}]`
67
+
68
+ Object.defineProperty(component, 'toString', {
69
+ value: toString,
70
+ enumerable: false,
71
+ })
72
+
73
+ Object.defineProperty(component, Symbol.for('nodejs.util.inspect.custom'), {
74
+ value: toString,
75
+ enumerable: false,
76
+ })
77
+
78
+ return component
79
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Prop validation utilities for UI components.
3
+ * @module core/PropValidation
4
+ */
5
+
6
+ /**
7
+ * Validates that a value is a valid Date object or a parseable date string.
8
+ * @param {any} value
9
+ * @param {string} propName
10
+ * @param {string} componentName
11
+ * @throws {TypeError} if validation fails
12
+ */
13
+ export function validateDate(value, propName, componentName) {
14
+ if (value === undefined || value === null) {
15
+ return // Optional prop
16
+ }
17
+
18
+ if (value instanceof Date) {
19
+ if (isNaN(value.getTime())) {
20
+ throw new TypeError(
21
+ `[${componentName}] Invalid Date object for prop "${propName}". Date is invalid.`
22
+ )
23
+ }
24
+ return
25
+ }
26
+
27
+ if (typeof value === 'string') {
28
+ const parsed = new Date(value)
29
+ if (isNaN(parsed.getTime())) {
30
+ throw new TypeError(
31
+ `[${componentName}] Invalid date string for prop "${propName}": "${value}". ` +
32
+ `Expected a valid ISO date string (e.g., "2026-02-05" or "2026-02-05T10:30:00").`
33
+ )
34
+ }
35
+ return
36
+ }
37
+
38
+ throw new TypeError(
39
+ `[${componentName}] Invalid type for prop "${propName}". ` +
40
+ `Expected Date object or date string, got ${typeof value}.`
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Validates that a value is a string.
46
+ * @param {any} value
47
+ * @param {string} propName
48
+ * @param {string} componentName
49
+ * @param {boolean} required
50
+ * @throws {TypeError} if validation fails
51
+ */
52
+ export function validateString(value, propName, componentName, required = false) {
53
+ if (value === undefined || value === null) {
54
+ if (required) {
55
+ throw new TypeError(`[${componentName}] Required prop "${propName}" is missing.`)
56
+ }
57
+ return
58
+ }
59
+
60
+ if (typeof value !== 'string') {
61
+ throw new TypeError(
62
+ `[${componentName}] Invalid type for prop "${propName}". ` +
63
+ `Expected string, got ${typeof value}.`
64
+ )
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Validates that a value is a function.
70
+ * @param {any} value
71
+ * @param {string} propName
72
+ * @param {string} componentName
73
+ * @param {boolean} required
74
+ * @throws {TypeError} if validation fails
75
+ */
76
+ export function validateFunction(value, propName, componentName, required = false) {
77
+ if (value === undefined || value === null) {
78
+ if (required) {
79
+ throw new TypeError(`[${componentName}] Required prop "${propName}" is missing.`)
80
+ }
81
+ return
82
+ }
83
+
84
+ if (typeof value !== 'function') {
85
+ throw new TypeError(
86
+ `[${componentName}] Invalid type for prop "${propName}". ` +
87
+ `Expected function, got ${typeof value}.`
88
+ )
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Validates that a value is a boolean.
94
+ * @param {any} value
95
+ * @param {string} propName
96
+ * @param {string} componentName
97
+ * @param {boolean} required
98
+ * @throws {TypeError} if validation fails
99
+ */
100
+ export function validateBoolean(value, propName, componentName, required = false) {
101
+ if (value === undefined || value === null) {
102
+ if (required) {
103
+ throw new TypeError(`[${componentName}] Required prop "${propName}" is missing.`)
104
+ }
105
+ return
106
+ }
107
+
108
+ if (typeof value !== 'boolean') {
109
+ throw new TypeError(
110
+ `[${componentName}] Invalid type for prop "${propName}". ` +
111
+ `Expected boolean, got ${typeof value}.`
112
+ )
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Validates that a value is a number.
118
+ * @param {any} value
119
+ * @param {string} propName
120
+ * @param {string} componentName
121
+ * @param {boolean} required
122
+ * @throws {TypeError} if validation fails
123
+ */
124
+ export function validateNumber(value, propName, componentName, required = false) {
125
+ if (value === undefined || value === null) {
126
+ if (required) {
127
+ throw new TypeError(`[${componentName}] Required prop "${propName}" is missing.`)
128
+ }
129
+ return
130
+ }
131
+
132
+ if (typeof value !== 'number' || isNaN(value)) {
133
+ throw new TypeError(
134
+ `[${componentName}] Invalid type for prop "${propName}". ` +
135
+ `Expected number, got ${typeof value}.`
136
+ )
137
+ }
138
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Component Renderer.
3
+ *
4
+ * Handles both Static Views and Interactive Prompts.
5
+ */
6
+
7
+ export async function render(component) {
8
+ // 1. Resolve Promises (if component is constructed async)
9
+ if (component instanceof Promise) {
10
+ component = await component
11
+ }
12
+
13
+ // 2. Handle null/undefined
14
+ if (!component) return
15
+
16
+ // 3. Handle Interactive Prompts (Active)
17
+ if (typeof component.execute === 'function') {
18
+ const result = await component.execute()
19
+ // If result is the unified return contract { value, cancelled }, unwrap it
20
+ if (result && typeof result === 'object' && 'value' in result) {
21
+ return result.value
22
+ }
23
+ return result
24
+ }
25
+
26
+ // 4. Handle Static Views (Passive)
27
+ // If it has a specific toString implementation (which our Views do), use it.
28
+ // Or if it's just a string.
29
+ const output = String(component)
30
+
31
+ // We append a newline for better UX in terminals, unless it's empty
32
+ if (output) {
33
+ process.stdout.write(output + '\n')
34
+ }
35
+
36
+ return output
37
+ }